mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
styling
This commit is contained in:
parent
b315766369
commit
8db4fbee07
12 changed files with 162 additions and 28 deletions
|
@ -27,10 +27,14 @@
|
||||||
<meta name="msapplication-TileColor" content="#E880C5">
|
<meta name="msapplication-TileColor" content="#E880C5">
|
||||||
<meta name="theme-color" content="#E880C5">
|
<meta name="theme-color" content="#E880C5">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<title>movie-web</title>
|
<title>movie-web</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript style="color: var(--text)">You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
export interface ButtonControlProps {
|
export interface ButtonControlProps {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonControl({ onClick, children }: ButtonControlProps) {
|
export function ButtonControl({ onClick, children, className }: ButtonControlProps) {
|
||||||
return <button onClick={onClick}>{children}</button>
|
return <button onClick={onClick} className={className}>{children}</button>
|
||||||
}
|
}
|
||||||
|
|
18
src/components/Buttons/IconButton.tsx
Normal file
18
src/components/Buttons/IconButton.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
||||||
|
import { Icon, Icons } from "components/Icon";
|
||||||
|
|
||||||
|
export interface IconButtonProps extends ButtonControlProps {
|
||||||
|
icon: Icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconButton(props: IconButtonProps) {
|
||||||
|
return (
|
||||||
|
<ButtonControl
|
||||||
|
{...props}
|
||||||
|
className="flex items-center px-4 py-2 space-x-2 bg-pink-900 text-white rounded-full"
|
||||||
|
>
|
||||||
|
<Icon icon={props.icon} />
|
||||||
|
<span>{props.children}</span>
|
||||||
|
</ButtonControl>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
export enum Icons {
|
export enum Icons {
|
||||||
SEARCH = "search",
|
SEARCH = "search",
|
||||||
|
BOOKMARK = "bookmark",
|
||||||
|
CLOCK = "clock",
|
||||||
|
EYE_SLASH = "eyeSlash",
|
||||||
|
ARROW_LEFT = "arrowLeft",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
|
@ -7,7 +11,11 @@ export interface IconProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconList = {
|
const iconList = {
|
||||||
search: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
|
search: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z"/></svg>`,
|
||||||
|
bookmark: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M384 48V512l-192-112L0 512V48C0 21.5 21.5 0 48 0h288C362.5 0 384 21.5 384 48z"/></svg>`,
|
||||||
|
clock: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512zM232 256C232 264 236 271.5 242.7 275.1L338.7 339.1C349.7 347.3 364.6 344.3 371.1 333.3C379.3 322.3 376.3 307.4 365.3 300L280 243.2V120C280 106.7 269.3 96 255.1 96C242.7 96 231.1 106.7 231.1 120L232 256z"/></svg>`,
|
||||||
|
eyeSlash: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M150.7 92.77C195 58.27 251.8 32 320 32C400.8 32 465.5 68.84 512.6 112.6C559.4 156 590.7 207.1 605.5 243.7C608.8 251.6 608.8 260.4 605.5 268.3C592.1 300.6 565.2 346.1 525.6 386.7L630.8 469.1C641.2 477.3 643.1 492.4 634.9 502.8C626.7 513.2 611.6 515.1 601.2 506.9L9.196 42.89C-1.236 34.71-3.065 19.63 5.112 9.196C13.29-1.236 28.37-3.065 38.81 5.112L150.7 92.77zM223.1 149.5L313.4 220.3C317.6 211.8 320 202.2 320 191.1C320 180.5 316.1 169.7 311.6 160.4C314.4 160.1 317.2 159.1 320 159.1C373 159.1 416 202.1 416 255.1C416 269.7 413.1 282.7 407.1 294.5L446.6 324.7C457.7 304.3 464 280.9 464 255.1C464 176.5 399.5 111.1 320 111.1C282.7 111.1 248.6 126.2 223.1 149.5zM320 480C239.2 480 174.5 443.2 127.4 399.4C80.62 355.1 49.34 304 34.46 268.3C31.18 260.4 31.18 251.6 34.46 243.7C44 220.8 60.29 191.2 83.09 161.5L177.4 235.8C176.5 242.4 176 249.1 176 255.1C176 335.5 240.5 400 320 400C338.7 400 356.6 396.4 373 389.9L446.2 447.5C409.9 467.1 367.8 480 320 480H320z"/></svg>`,
|
||||||
|
arrowLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M9.375 233.4l128-128c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L109.3 224H480c17.69 0 32 14.31 32 32s-14.31 32-32 32H109.3l73.38 73.38c12.5 12.5 12.5 32.75 0 45.25c-12.49 12.49-32.74 12.51-45.25 0l-128-128C-3.125 266.1-3.125 245.9 9.375 233.4z"/></svg>`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Icon(props: IconProps) {
|
export function Icon(props: IconProps) {
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
import { ButtonControl } from './Buttons/ButtonControl';
|
import { IconButton } from "./Buttons/IconButton";
|
||||||
import { Icon, Icons } from './Icon';
|
import { Icons } from "./Icon";
|
||||||
import { TextInputControl, TextInputControlPropsNoLabel } from './TextInputs/TextInputControl';
|
import {
|
||||||
|
TextInputControl,
|
||||||
|
TextInputControlPropsNoLabel,
|
||||||
|
} from "./TextInputs/TextInputControl";
|
||||||
|
|
||||||
export interface SearchBarProps extends TextInputControlPropsNoLabel {
|
export interface SearchBarProps extends TextInputControlPropsNoLabel {
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchBarInput(props: SearchBarProps) {
|
export function SearchBarInput(props: SearchBarProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex items-center space-x-4 pl-8 pr-2 py-2 bg-dink-500 rounded-full">
|
||||||
<TextInputControl onChange={props.onChange} value={props.value} />
|
<TextInputControl
|
||||||
<ButtonControl onClick={props.onClick}>
|
onChange={props.onChange}
|
||||||
<Icon icon={Icons.SEARCH} />
|
value={props.value}
|
||||||
|
className="placeholder-dink-150 bg-transparent flex-1 focus:outline-none text-white"
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
/>
|
||||||
|
<IconButton icon={Icons.SEARCH} onClick={props.onClick}>
|
||||||
{props.buttonText || "Search"}
|
{props.buttonText || "Search"}
|
||||||
</ButtonControl>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,30 @@
|
||||||
export interface TextInputControlPropsNoLabel {
|
export interface TextInputControlPropsNoLabel {
|
||||||
onChange?: (data: string) => void;
|
onChange?: (data: string) => void;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextInputControlProps extends TextInputControlPropsNoLabel {
|
export interface TextInputControlProps extends TextInputControlPropsNoLabel {
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TextInputControl({ onChange, value, label }: TextInputControlProps) {
|
export function TextInputControl({
|
||||||
const input = <input type="text" onChange={(e) => onChange && onChange(e.target.value)} value={value} />
|
onChange,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
placeholder,
|
||||||
|
}: TextInputControlProps) {
|
||||||
|
const input = (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={className}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(e) => onChange && onChange(e.target.value)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
if (label) {
|
if (label) {
|
||||||
return (
|
return (
|
||||||
|
@ -16,7 +32,7 @@ export function TextInputControl({ onChange, value, label }: TextInputControlPro
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{input}
|
{input}
|
||||||
</label>
|
</label>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
|
|
24
src/components/layout/SectionHeading.tsx
Normal file
24
src/components/layout/SectionHeading.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { Icon, Icons } from "components/Icon";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface SectionHeadingProps {
|
||||||
|
icon?: Icons;
|
||||||
|
title: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionHeading(props: SectionHeadingProps) {
|
||||||
|
return (
|
||||||
|
<div className="mt-12">
|
||||||
|
<p className="flex items-center uppercase font-bold text-dink-300 mb-3">
|
||||||
|
{props.icon ? (
|
||||||
|
<span className="text-xl mr-2">
|
||||||
|
<Icon icon={props.icon} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{props.title}
|
||||||
|
</p>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
12
src/components/layout/ThinContainer.tsx
Normal file
12
src/components/layout/ThinContainer.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface ThinContainerProps {
|
||||||
|
classNames?: string,
|
||||||
|
children?: ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThinContainer(props: ThinContainerProps) {
|
||||||
|
return (<div className={`max-w-[600px] mx-auto ${props.classNames || ""}`}>
|
||||||
|
{props.children}
|
||||||
|
</div>)
|
||||||
|
}
|
|
@ -1,3 +1,8 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
@apply min-h-screen bg-dink-900 text-white font-open-sans;
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const theFlixMovieScraper: MWMediaProvider = {
|
||||||
async getMediaFromPortable(media: MWPortableMedia): Promise<MWMedia> {
|
async getMediaFromPortable(media: MWPortableMedia): Promise<MWMedia> {
|
||||||
return {
|
return {
|
||||||
...media,
|
...media,
|
||||||
title: "title here"
|
title: "title is here"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export const theFlixMovieScraper: MWMediaProvider = {
|
||||||
return [{
|
return [{
|
||||||
mediaId: "a",
|
mediaId: "a",
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
title: `movie test`,
|
title: `movie testing in progress`,
|
||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,22 +2,45 @@ import { WatchedMediaCard } from "components/media/WatchedMediaCard";
|
||||||
import { SearchBarInput } from "components/SearchBar";
|
import { SearchBarInput } from "components/SearchBar";
|
||||||
import { MWMedia, MWMediaType, SearchProviders } from "scrapers";
|
import { MWMedia, MWMediaType, SearchProviders } from "scrapers";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { ThinContainer } from "components/layout/ThinContainer";
|
||||||
|
import { SectionHeading } from "components/layout/SectionHeading";
|
||||||
|
import { Icons } from "components/Icon";
|
||||||
|
|
||||||
export function SearchView() {
|
export function SearchView() {
|
||||||
const [results, setResults] = useState<MWMedia[]>([]);
|
const [results, setResults] = useState<MWMedia[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
async function runSearch() {
|
async function runSearch() {
|
||||||
const results = await SearchProviders({ type: MWMediaType.MOVIE, searchQuery: search });
|
const results = await SearchProviders({
|
||||||
|
type: MWMediaType.MOVIE,
|
||||||
|
searchQuery: search,
|
||||||
|
});
|
||||||
setResults(results);
|
setResults(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ThinContainer>
|
||||||
<h1>Search</h1>
|
<div className="mt-36 text-center space-y-16">
|
||||||
<SearchBarInput onChange={setSearch} value={search} onClick={runSearch}/>
|
<div className="space-y-4">
|
||||||
<h1 className="bg-red-500">Search results</h1>
|
<p className="font-bold text-bink">
|
||||||
{results.map((v)=>(<WatchedMediaCard media={v} />))}
|
Because watching legally is boring
|
||||||
|
</p>
|
||||||
|
<h1 className="text-4xl font-bold">
|
||||||
|
What movie do you want to watch?
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
)
|
<SearchBarInput
|
||||||
|
onChange={setSearch}
|
||||||
|
value={search}
|
||||||
|
onClick={runSearch}
|
||||||
|
placeholder="What movie do you want to watch?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SectionHeading title="Yoink" icon={Icons.SEARCH}>
|
||||||
|
{results.map((v) => (
|
||||||
|
<WatchedMediaCard media={v} />
|
||||||
|
))}
|
||||||
|
</SectionHeading>
|
||||||
|
</ThinContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
"dink-900": "#131119",
|
||||||
|
"dink-500": "#252037",
|
||||||
|
"dink-400": "#231F34",
|
||||||
|
"dink-300": "#70688B",
|
||||||
|
"dink-200": "#3A364D",
|
||||||
|
"dink-150": "#8F87AB",
|
||||||
|
"dink-100": "#393447",
|
||||||
|
bink: "#D588E3",
|
||||||
|
"pink-900": "#412B57",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
"open-sans": "'Open Sans'",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue