mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-14 07:35:06 +01:00
search page styling
This commit is contained in:
parent
01d18aa459
commit
e75fcd3002
134
src/components/Buttons/DropdownButton.tsx
Normal file
134
src/components/Buttons/DropdownButton.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
||||||
|
import { Icon, Icons } from "components/Icon";
|
||||||
|
import React, {
|
||||||
|
useRef,
|
||||||
|
Ref,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
MouseEventHandler,
|
||||||
|
KeyboardEvent,
|
||||||
|
SyntheticEvent,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import { Backdrop, useBackdrop } from "components/layout/Backdrop";
|
||||||
|
|
||||||
|
export interface DropdownButtonProps extends ButtonControlProps {
|
||||||
|
icon: Icons;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
selectedItem: string;
|
||||||
|
setSelectedItem: Dispatch<SetStateAction<string>>;
|
||||||
|
options: Array<OptionItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionProps {
|
||||||
|
option: OptionItem;
|
||||||
|
onClick: MouseEventHandler<HTMLDivElement>;
|
||||||
|
tabIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: Icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Option({ option, onClick, tabIndex }: OptionProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="text-denim-700 h-10 px-4 py-2 text-left cursor-pointer flex items-center space-x-2 hover:text-white transition-colors"
|
||||||
|
onClick={onClick}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
>
|
||||||
|
<Icon icon={option.icon} />
|
||||||
|
<input type="radio" className="hidden" id={option.id} />
|
||||||
|
<label htmlFor={option.id} className="cursor-pointer ">
|
||||||
|
<div className="item">{option.name}</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropdownButton = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
DropdownButtonProps
|
||||||
|
>((props, ref) => {
|
||||||
|
const [setBackdrop, backdropProps, highlightedProps] = useBackdrop();
|
||||||
|
const [delayedSelectedId, setDelayedSelectedId] = useState(
|
||||||
|
props.selectedItem
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let id: NodeJS.Timeout;
|
||||||
|
|
||||||
|
if (props.open) {
|
||||||
|
setDelayedSelectedId(props.selectedItem);
|
||||||
|
} else {
|
||||||
|
id = setTimeout(() => {
|
||||||
|
setDelayedSelectedId(props.selectedItem);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (id) clearTimeout(id);
|
||||||
|
};
|
||||||
|
}, [props.open]);
|
||||||
|
|
||||||
|
const selectedItem: OptionItem = props.options.find(
|
||||||
|
(opt) => opt.id === props.selectedItem
|
||||||
|
) || { id: "movie", name: "movie", icon: Icons.ARROW_LEFT };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBackdrop(props.open);
|
||||||
|
}, [props.open]);
|
||||||
|
|
||||||
|
const onOptionClick = (e: SyntheticEvent, option: OptionItem) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.setSelectedItem(option.id);
|
||||||
|
props.setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full sm:w-auto min-w-[140px]">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="relative w-full sm:w-auto"
|
||||||
|
{...highlightedProps}
|
||||||
|
>
|
||||||
|
<ButtonControl
|
||||||
|
{...props}
|
||||||
|
className="flex items-center justify-center sm:justify-left px-4 py-2 space-x-2 bg-bink-200 relative z-20 hover:bg-bink-300 text-white h-10 rounded-[20px] w-full"
|
||||||
|
>
|
||||||
|
<Icon icon={selectedItem.icon} />
|
||||||
|
<span className="flex-1">{selectedItem.name}</span>
|
||||||
|
<Icon
|
||||||
|
icon={Icons.CHEVRON_DOWN}
|
||||||
|
className={`transition-transform ${props.open ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</ButtonControl>
|
||||||
|
<div
|
||||||
|
className={`absolute pt-[40px] top-0 duration-200 transition-all w-full rounded-[20px] z-10 bg-denim-300 ${
|
||||||
|
props.open
|
||||||
|
? "opacity-100 max-h-60 block"
|
||||||
|
: "opacity-0 max-h-0 invisible"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{props.options
|
||||||
|
.filter((opt) => opt.id != delayedSelectedId)
|
||||||
|
.map((opt) => (
|
||||||
|
<Option
|
||||||
|
option={opt}
|
||||||
|
key={opt.id}
|
||||||
|
onClick={(e) => onOptionClick(e, opt)}
|
||||||
|
tabIndex={
|
||||||
|
props.open ? 0 : undefined
|
||||||
|
} /*onKeyPress={active ? handleOptionKeyPress(opt, i) : undefined}*/
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Backdrop onClick={() => props.setOpen(false)} {...backdropProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -9,7 +9,7 @@ export function IconButton(props: IconButtonProps) {
|
|||||||
return (
|
return (
|
||||||
<ButtonControl
|
<ButtonControl
|
||||||
{...props}
|
{...props}
|
||||||
className="flex items-center px-4 py-2 space-x-2 bg-pink-900 text-white rounded-full"
|
className="flex items-center px-4 py-2 space-x-2 bg-bink-200 hover:bg-bink-300 text-white rounded-full"
|
||||||
>
|
>
|
||||||
<Icon icon={props.icon} />
|
<Icon icon={props.icon} />
|
||||||
<span>{props.children}</span>
|
<span>{props.children}</span>
|
||||||
|
@ -4,10 +4,15 @@ export enum Icons {
|
|||||||
CLOCK = "clock",
|
CLOCK = "clock",
|
||||||
EYE_SLASH = "eyeSlash",
|
EYE_SLASH = "eyeSlash",
|
||||||
ARROW_LEFT = "arrowLeft",
|
ARROW_LEFT = "arrowLeft",
|
||||||
|
CHEVRON_DOWN = "chevronDown",
|
||||||
|
CLAPPER_BOARD = "clapperBoard",
|
||||||
|
FILM = "film",
|
||||||
|
DRAGON = "dragon",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
icon: Icons;
|
icon: Icons;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconList = {
|
const iconList = {
|
||||||
@ -16,8 +21,17 @@ const iconList = {
|
|||||||
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>`,
|
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>`,
|
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>`,
|
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>`,
|
||||||
}
|
chevronDown: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>`,
|
||||||
|
clapperBoard: `<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="M326.1 160l127.4-127.4C451.7 32.39 449.9 32 448 32h-86.06l-128 128H326.1zM166.1 160l128-128H201.9l-128 128H166.1zM497.7 56.19L393.9 160H512V96C512 80.87 506.5 67.15 497.7 56.19zM134.1 32H64C28.65 32 0 60.65 0 96v64h6.062L134.1 32zM0 416c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V192H0V416z"/></svg>`,
|
||||||
|
film: `<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="M463.1 32h-416C21.49 32-.0001 53.49-.0001 80v352c0 26.51 21.49 48 47.1 48h416c26.51 0 48-21.49 48-48v-352C511.1 53.49 490.5 32 463.1 32zM111.1 408c0 4.418-3.582 8-8 8H55.1c-4.418 0-8-3.582-8-8v-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8L111.1 408zM111.1 280c0 4.418-3.582 8-8 8H55.1c-4.418 0-8-3.582-8-8v-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8V280zM111.1 152c0 4.418-3.582 8-8 8H55.1c-4.418 0-8-3.582-8-8v-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8L111.1 152zM351.1 400c0 8.836-7.164 16-16 16H175.1c-8.836 0-16-7.164-16-16v-96c0-8.838 7.164-16 16-16h160c8.836 0 16 7.162 16 16V400zM351.1 208c0 8.836-7.164 16-16 16H175.1c-8.836 0-16-7.164-16-16v-96c0-8.838 7.164-16 16-16h160c8.836 0 16 7.162 16 16V208zM463.1 408c0 4.418-3.582 8-8 8h-47.1c-4.418 0-7.1-3.582-7.1-8l0-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8V408zM463.1 280c0 4.418-3.582 8-8 8h-47.1c-4.418 0-8-3.582-8-8v-48c0-4.418 3.582-8 8-8h47.1c4.418 0 8 3.582 8 8V280zM463.1 152c0 4.418-3.582 8-8 8h-47.1c-4.418 0-8-3.582-8-8l0-48c0-4.418 3.582-8 7.1-8h47.1c4.418 0 8 3.582 8 8V152z"/></svg>`,
|
||||||
|
dragon: `<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="M18.43 255.8L192 224L100.8 292.6C90.67 302.8 97.8 320 112 320h222.7c-9.499-26.5-14.75-54.5-14.75-83.38V194.2L200.3 106.8C176.5 90.88 145 92.75 123.3 111.2l-117.5 116.4C-6.562 238 2.436 258 18.43 255.8zM575.2 289.9l-100.7-50.25c-16.25-8.125-26.5-24.75-26.5-43V160h63.99l28.12 22.62C546.1 188.6 554.2 192 562.7 192h30.1c11.1 0 23.12-6.875 28.5-17.75l14.37-28.62c5.374-10.87 4.25-23.75-2.999-33.5l-74.49-99.37C552.1 4.75 543.5 0 533.5 0H296C288.9 0 285.4 8.625 290.4 13.62L351.1 64L292.4 88.75c-5.874 3-5.874 11.37 0 14.37L351.1 128l-.0011 108.6c0 72 35.99 139.4 95.99 179.4c-195.6 6.75-344.4 41-434.1 60.88c-8.124 1.75-13.87 9-13.87 17.38C.0463 504 8.045 512 17.79 512h499.1c63.24 0 119.6-47.5 122.1-110.8C642.3 354 617.1 310.9 575.2 289.9zM489.1 66.25l45.74 11.38c-2.75 11-12.5 18.88-24.12 18.25C497.7 95.25 484.8 83.38 489.1 66.25z"/></svg>`,
|
||||||
|
};
|
||||||
|
|
||||||
export function Icon(props: IconProps) {
|
export function Icon(props: IconProps) {
|
||||||
return <span dangerouslySetInnerHTML={{ __html: iconList[props.icon] }} />;
|
return (
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{ __html: iconList[props.icon] }}
|
||||||
|
className={props.className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { IconButton } from "./Buttons/IconButton";
|
import { DropdownButton } from "./Buttons/DropdownButton";
|
||||||
import { Icons } from "./Icon";
|
import { Icons } from "./Icon";
|
||||||
import {
|
import {
|
||||||
TextInputControl,
|
TextInputControl,
|
||||||
TextInputControlPropsNoLabel,
|
TextInputControlPropsNoLabel,
|
||||||
} from "./TextInputs/TextInputControl";
|
} from "./TextInputs/TextInputControl";
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
export interface SearchBarProps extends TextInputControlPropsNoLabel {
|
export interface SearchBarProps extends TextInputControlPropsNoLabel {
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@ -12,17 +14,61 @@ export interface SearchBarProps extends TextInputControlPropsNoLabel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SearchBarInput(props: SearchBarProps) {
|
export function SearchBarInput(props: SearchBarProps) {
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [dropdownSelected, setDropdownSelected] = useState("movie");
|
||||||
|
|
||||||
|
const dropdownRef = useRef<any>();
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (dropdownRef.current?.contains(e.target as Node)) {
|
||||||
|
// inside click
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// outside click
|
||||||
|
closeDropdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdown = () => {
|
||||||
|
setDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-4 pl-8 pr-2 py-2 bg-dink-500 rounded-full">
|
<div className="flex flex-col sm:flex-row items-center gap-4 px-4 py-4 sm:pl-8 sm:pr-2 sm:py-2 bg-denim-300 rounded-[28px] hover:bg-denim-400 focus-within:bg-denim-400 transition-colors">
|
||||||
<TextInputControl
|
<TextInputControl
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
className="placeholder-dink-150 bg-transparent flex-1 focus:outline-none text-white"
|
className="placeholder-denim-700 w-full bg-transparent flex-1 focus:outline-none text-white"
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
/>
|
/>
|
||||||
<IconButton icon={Icons.SEARCH} onClick={props.onClick}>
|
|
||||||
|
<DropdownButton
|
||||||
|
icon={Icons.SEARCH}
|
||||||
|
open={dropdownOpen}
|
||||||
|
setOpen={setDropdownOpen}
|
||||||
|
selectedItem={dropdownSelected}
|
||||||
|
setSelectedItem={setDropdownSelected}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
id: "movie",
|
||||||
|
name: "Movie",
|
||||||
|
icon: Icons.FILM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "series",
|
||||||
|
name: "Series",
|
||||||
|
icon: Icons.CLAPPER_BOARD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "anime",
|
||||||
|
name: "Anime",
|
||||||
|
icon: Icons.DRAGON,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onClick={() => setDropdownOpen((old) => !old)}
|
||||||
|
ref={dropdownRef}
|
||||||
|
>
|
||||||
{props.buttonText || "Search"}
|
{props.buttonText || "Search"}
|
||||||
</IconButton>
|
</DropdownButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
7
src/components/Text/Tagline.tsx
Normal file
7
src/components/Text/Tagline.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface TaglineProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tagline(props: TaglineProps) {
|
||||||
|
return <p className="font-bold text-bink-600">{props.children}</p>;
|
||||||
|
}
|
7
src/components/Text/Title.tsx
Normal file
7
src/components/Text/Title.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface TitleProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Title(props: TitleProps) {
|
||||||
|
return <h1 className="text-4xl font-bold text-white">{props.children}</h1>;
|
||||||
|
}
|
66
src/components/layout/Backdrop.tsx
Normal file
66
src/components/layout/Backdrop.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { useFade } from "hooks/useFade";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface BackdropProps {
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
onBackdropHide?: () => void;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBackdrop(): [
|
||||||
|
(state: boolean) => void,
|
||||||
|
BackdropProps,
|
||||||
|
{ style: any }
|
||||||
|
] {
|
||||||
|
const [backdrop, setBackdropState] = useState(false);
|
||||||
|
const [isHighlighted, setisHighlighted] = useState(false);
|
||||||
|
|
||||||
|
const setBackdrop = (state: boolean) => {
|
||||||
|
setBackdropState(state);
|
||||||
|
if (state) setisHighlighted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const backdropProps: BackdropProps = {
|
||||||
|
active: backdrop,
|
||||||
|
onBackdropHide() {
|
||||||
|
setisHighlighted(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightedProps = {
|
||||||
|
style: isHighlighted
|
||||||
|
? {
|
||||||
|
zIndex: "1000",
|
||||||
|
position: "relative",
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [setBackdrop, backdropProps, highlightedProps];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Backdrop(props: BackdropProps) {
|
||||||
|
const clickEvent = props.onClick || ((e: MouseEvent) => {});
|
||||||
|
const animationEvent = props.onBackdropHide || (() => {});
|
||||||
|
const [isVisible, setVisible, fadeProps] = useFade();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVisible(!!props.active);
|
||||||
|
}, [props.active]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVisible) animationEvent();
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed h-screen z-[999] top-0 left-0 right-0 bg-black bg-opacity-50 transition-opacity opacity-100 ${
|
||||||
|
!isVisible ? "opacity-0" : ""
|
||||||
|
}`}
|
||||||
|
{...fadeProps}
|
||||||
|
onClick={(e) => clickEvent(e.nativeEvent)}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
}
|
10
src/components/layout/Loading.tsx
Normal file
10
src/components/layout/Loading.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center h-12 justify-center">
|
||||||
|
<div className="w-2 mx-1 h-2 rounded-full animate-loading-pin bg-denim-300"></div>
|
||||||
|
<div className="w-2 mx-1 h-2 rounded-full [animation-delay:150ms] animate-loading-pin bg-denim-300"></div>
|
||||||
|
<div className="w-2 mx-1 h-2 rounded-full [animation-delay:300ms] animate-loading-pin bg-denim-300"></div>
|
||||||
|
<div className="w-2 mx-1 h-2 rounded-full [animation-delay:450ms] animate-loading-pin bg-denim-300"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -10,7 +10,7 @@ interface SectionHeadingProps {
|
|||||||
export function SectionHeading(props: SectionHeadingProps) {
|
export function SectionHeading(props: SectionHeadingProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<p className="flex items-center uppercase font-bold text-dink-300 mb-3">
|
<p className="flex items-center uppercase font-bold text-denim-700 mb-3">
|
||||||
{props.icon ? (
|
{props.icon ? (
|
||||||
<span className="text-xl mr-2">
|
<span className="text-xl mr-2">
|
||||||
<Icon icon={props.icon} />
|
<Icon icon={props.icon} />
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
interface ThinContainerProps {
|
interface ThinContainerProps {
|
||||||
classNames?: string,
|
classNames?: string;
|
||||||
children?: ReactNode,
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThinContainer(props: ThinContainerProps) {
|
export function ThinContainer(props: ThinContainerProps) {
|
||||||
return (<div className={`max-w-[600px] mx-auto ${props.classNames || ""}`}>
|
return (
|
||||||
|
<div
|
||||||
|
className={`max-w-[600px] mx-auto px-2 sm:px-0 ${props.classNames || ""}`}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>)
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
0
src/components/layout/loading.css
Normal file
0
src/components/layout/loading.css
Normal file
17
src/hooks/useFade.css
Normal file
17
src/hooks/useFade.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
27
src/hooks/useFade.ts
Normal file
27
src/hooks/useFade.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import './useFade.css'
|
||||||
|
|
||||||
|
export const useFade = (initial: boolean = false): [boolean, React.Dispatch<React.SetStateAction<boolean>>, any] => {
|
||||||
|
const [show, setShow] = useState<boolean>(initial);
|
||||||
|
const [isVisible, setVisible] = useState<boolean>(show);
|
||||||
|
|
||||||
|
// Update visibility when show changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) setVisible(true);
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
// When the animation finishes, set visibility to false
|
||||||
|
const onAnimationEnd = () => {
|
||||||
|
if (!show) setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = { animation: `${show ? "fadeIn" : "fadeOut"} .3s` };
|
||||||
|
|
||||||
|
// These props go on the fading DOM element
|
||||||
|
const fadeProps = {
|
||||||
|
style,
|
||||||
|
onAnimationEnd
|
||||||
|
};
|
||||||
|
|
||||||
|
return [isVisible, setShow, fadeProps];
|
||||||
|
};
|
@ -5,6 +5,9 @@ import { useState } from "react";
|
|||||||
import { ThinContainer } from "components/layout/ThinContainer";
|
import { ThinContainer } from "components/layout/ThinContainer";
|
||||||
import { SectionHeading } from "components/layout/SectionHeading";
|
import { SectionHeading } from "components/layout/SectionHeading";
|
||||||
import { Icons } from "components/Icon";
|
import { Icons } from "components/Icon";
|
||||||
|
import { Loading } from "components/layout/Loading";
|
||||||
|
import { Tagline } from "components/Text/Tagline";
|
||||||
|
import { Title } from "components/Text/Title";
|
||||||
|
|
||||||
export function SearchView() {
|
export function SearchView() {
|
||||||
const [results, setResults] = useState<MWMedia[]>([]);
|
const [results, setResults] = useState<MWMedia[]>([]);
|
||||||
@ -22,12 +25,8 @@ export function SearchView() {
|
|||||||
<ThinContainer>
|
<ThinContainer>
|
||||||
<div className="mt-36 text-center space-y-16">
|
<div className="mt-36 text-center space-y-16">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="font-bold text-bink">
|
<Tagline>Because watching legally is boring</Tagline>
|
||||||
Because watching legally is boring
|
<Title>What movie do you want to watch?</Title>
|
||||||
</p>
|
|
||||||
<h1 className="text-4xl font-bold">
|
|
||||||
What movie do you want to watch?
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<SearchBarInput
|
<SearchBarInput
|
||||||
onChange={setSearch}
|
onChange={setSearch}
|
||||||
@ -41,6 +40,7 @@ export function SearchView() {
|
|||||||
<WatchedMediaCard media={v} />
|
<WatchedMediaCard media={v} />
|
||||||
))}
|
))}
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
|
<Loading />
|
||||||
</ThinContainer>
|
</ThinContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,37 @@ module.exports = {
|
|||||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
/* colors */
|
||||||
colors: {
|
colors: {
|
||||||
"dink-900": "#131119",
|
"bink-100": "#432449",
|
||||||
"dink-500": "#252037",
|
"bink-200": "#412B57",
|
||||||
"dink-400": "#231F34",
|
"bink-300": "#533670",
|
||||||
"dink-300": "#70688B",
|
"bink-400": "#714C97",
|
||||||
"dink-200": "#3A364D",
|
"bink-500": "#8D66B5",
|
||||||
"dink-150": "#8F87AB",
|
"bink-600": "#A87FD1",
|
||||||
"dink-100": "#393447",
|
"bink-700": "#CD97D6",
|
||||||
bink: "#D588E3",
|
"denim-100": "#131119",
|
||||||
"pink-900": "#412B57",
|
"denim-200": "#1E1A29",
|
||||||
|
"denim-300": "#282336",
|
||||||
|
"denim-400": "#322D43",
|
||||||
|
"denim-500": "#433D55",
|
||||||
|
"denim-600": "#5A5370",
|
||||||
|
"denim-700": "#817998",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* fonts */
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
"open-sans": "'Open Sans'",
|
"open-sans": "'Open Sans'",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* animations */
|
||||||
|
keyframes: {
|
||||||
|
"loading-pin": {
|
||||||
|
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" },
|
||||||
|
"20%": { height: "1em", "background-color": "white" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
Loading…
Reference in New Issue
Block a user