feat(search): add basic search functionality
this commit adds basic search feature. fix: https://codeberg.org/zyachel/libremdb/issues/9, https://github.com/zyachel/libremdb/issues/10
This commit is contained in:
parent
81eaf2fd5e
commit
0cff34a766
25 changed files with 1191 additions and 60 deletions
|
@ -2,14 +2,21 @@
|
|||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
async redirects() {
|
||||
return [
|
||||
async rewrites() {
|
||||
return {
|
||||
afterFiles: [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/about',
|
||||
permanent: true,
|
||||
destination: '/find',
|
||||
},
|
||||
];
|
||||
],
|
||||
fallback: [
|
||||
{
|
||||
source: '/:path*',
|
||||
destination: '/404',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
images: {
|
||||
domains: ['m.media-amazon.com'],
|
||||
|
@ -20,6 +27,7 @@ const nextConfig = {
|
|||
},
|
||||
isrMemoryCacheSize: 20 * 1024 * 1024,
|
||||
},
|
||||
poweredByHeader: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
@ -15,42 +15,19 @@
|
|||
</symbol>
|
||||
|
||||
<!--miscellaneous logos-->
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-cancel">
|
||||
<path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM64 256c0-41.4 13.3-79.68 35.68-111.1l267.4 267.4C335.7 434.7 297.4 448 256 448C150.1 448 64 361.9 64 256zM412.3 367.1L144.9 99.68C176.3 77.3 214.6 64 256 64c105.9 0 192 86.13 192 192C448 297.4 434.7 335.7 412.3 367.1z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" id="icon-code-document">
|
||||
<path d="M162.1 257.8c-7.812-7.812-20.47-7.812-28.28 0l-48 48c-7.812 7.812-7.812 20.5 0 28.31l48 48C137.8 386.1 142.9 388 148 388s10.23-1.938 14.14-5.844c7.812-7.812 7.812-20.5 0-28.31L128.3 320l33.86-33.84C169.1 278.3 169.1 265.7 162.1 257.8zM365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM221.9 257.8c-7.812 7.812-7.812 20.5 0 28.31L255.7 320l-33.86 33.84c-7.812 7.812-7.812 20.5 0 28.31C225.8 386.1 230.9 388 236 388s10.23-1.938 14.14-5.844l48-48c7.812-7.812 7.812-20.5 0-28.31l-48-48C242.3 250 229.7 250 221.9 257.8z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-computer-home">
|
||||
<path d="M218.3 8.486C230.6-2.829 249.4-2.829 261.7 8.486L469.7 200.5C476.4 206.7 480 215.2 480 224H336C316.9 224 299.7 232.4 288 245.7V208C288 199.2 280.8 192 272 192H208C199.2 192 192 199.2 192 208V272C192 280.8 199.2 288 208 288H271.1V416H112C85.49 416 64 394.5 64 368V256H32C18.83 256 6.996 247.9 2.198 235.7C-2.6 223.4 .6145 209.4 10.3 200.5L218.3 8.486zM336 256H560C577.7 256 592 270.3 592 288V448H624C632.8 448 640 455.2 640 464C640 490.5 618.5 512 592 512H303.1C277.5 512 255.1 490.5 255.1 464C255.1 455.2 263.2 448 271.1 448H303.1V288C303.1 270.3 318.3 256 336 256zM352 304V448H544V304H352z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-link-slash">
|
||||
<path d="M485.1 354.9l113.5-113.5c55.21-55.21 55.21-144.7 0-199.9C570.1 13.8 534.8 0 498.6 0s-72.36 13.8-99.96 41.41l-43.36 43.36c15.11 8.012 29.47 17.58 41.91 30.02c3.146 3.146 5.898 6.518 8.742 9.838l37.96-37.96C458.5 72.05 477.1 64 498.6 64s40.1 8.047 54.71 22.66c14.61 14.61 22.66 34.04 22.66 54.71s-8.049 40.1-22.66 54.71l-119 119l-30.09-23.59c21.49-51.28 12.12-112.4-29.63-154.1C346.1 109.8 310.8 96 274.6 96c-29.6 0-58.93 9.752-83.83 28.23L38.81 5.109C34.41 1.672 29.19 0 24.03 0c-7.125 0-14.19 3.156-18.91 9.187c-8.188 10.44-6.375 25.53 4.062 33.7l591.1 463.1c10.5 8.203 25.56 6.328 33.69-4.078c8.188-10.44 6.375-25.53-4.062-33.7L485.1 354.9zM350.8 249.6L244.3 166.2C253.8 162.2 264 160 274.6 160c20.67 0 40.1 8.049 54.71 22.66c14.62 14.61 22.66 34.04 22.66 54.71C352 241.5 351.4 245.6 350.8 249.6zM234 387.4l-37.96 37.96C181.5 439.1 162 448 141.4 448c-20.67 0-40.1-8.047-54.71-22.66c-14.61-14.61-22.66-34.04-22.66-54.71s8.049-40.1 22.66-54.71l84.83-84.83L120.7 191.3L41.41 270.7c-55.21 55.21-55.21 144.7 0 199.9C69.01 498.2 105.2 512 141.4 512c36.18 0 72.36-13.8 99.96-41.41l43.36-43.36c-15.11-8.012-29.47-17.58-41.91-30.02C239.6 394.1 236.9 390.7 234 387.4zM265.4 374.6C293 402.2 329.2 416 365.4 416c11.98 0 23.84-2.082 35.51-5.111L224.6 272.7C223.9 309.5 237.3 346.5 265.4 374.6z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-eye-slash">
|
||||
<path d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-feather">
|
||||
<path d="M483.4 244.2L351.9 287.1h97.74c-9.874 10.62 3.75-3.125-46.24 46.87l-147.6 49.12h98.24c-74.99 73.12-194.6 70.62-246.8 54.1l-66.14 65.99c-9.374 9.374-24.6 9.374-33.98 0s-9.374-24.6 0-33.98l259.5-259.2c6.249-6.25 6.249-16.37 0-22.62c-6.249-6.249-16.37-6.249-22.62 0l-178.4 178.2C58.78 306.1 68.61 216.7 129.1 156.3l85.74-85.68c90.62-90.62 189.8-88.27 252.3-25.78C517.8 95.34 528.9 169.7 483.4 244.2z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-fast-forward">
|
||||
<path d="M52.51 440.6l171.5-142.9V214.3L52.51 71.41C31.88 54.28 0 68.66 0 96.03v319.9C0 443.3 31.88 457.7 52.51 440.6zM308.5 440.6l192-159.1c15.25-12.87 15.25-36.37 0-49.24l-192-159.1c-20.63-17.12-52.51-2.749-52.51 24.62v319.9C256 443.3 287.9 457.7 308.5 440.6z"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-graph-rising">
|
||||
<path d="M472 432h-48a24 24 0 01-24-24V104a24 24 0 0124-24h48a24 24 0 0124 24v304a24 24 0 01-24 24zM344 432h-48a24 24 0 01-24-24V184a24 24 0 0124-24h48a24 24 0 0124 24v224a24 24 0 01-24 24zM216 432h-48a24 24 0 01-24-24V248a24 24 0 0124-24h48a24 24 0 0124 24v160a24 24 0 01-24 24zM88 432H40a24 24 0 01-24-24v-96a24 24 0 0124-24h48a24 24 0 0124 24v96a24 24 0 01-24 24z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" id="icon-rating">
|
||||
<path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-rewind">
|
||||
<path d="M459.5 71.41l-171.5 142.9v83.45l171.5 142.9C480.1 457.7 512 443.3 512 415.1V96.03C512 68.66 480.1 54.28 459.5 71.41zM203.5 71.41L11.44 231.4c-15.25 12.87-15.25 36.37 0 49.24l192 159.1c20.63 17.12 52.51 2.749 52.51-24.62v-319.9C255.1 68.66 224.1 54.28 203.5 71.41z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-like-dislike">
|
||||
<path d="M22.5,10H15.75C15.13,10 14.6,10.38 14.37,10.91L12.11,16.2C12.04,16.37 12,16.56 12,16.75V18A1,1 0 0,0 13,19H18.18L17.5,22.18V22.42C17.5,22.73 17.63,23 17.83,23.22L18.62,24L23.56,19.06C23.83,18.79 24,18.41 24,18V11.5A1.5,1.5 0 0,0 22.5,10M12,6A1,1 0 0,0 11,5H5.82L6.5,1.82V1.59C6.5,1.28 6.37,1 6.17,0.79L5.38,0L0.44,4.94C0.17,5.21 0,5.59 0,6V12.5A1.5,1.5 0 0,0 1.5,14H8.25C8.87,14 9.4,13.62 9.63,13.09L11.89,7.8C11.96,7.63 12,7.44 12,7.25V6Z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-person-slash">
|
||||
<path d="M95.1 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7c5.625 0 10.73-1.65 15.42-4.029L264.9 304.3C171.3 306.7 95.1 383.1 95.1 477.3zM630.8 469.1l-277.1-217.9c54.69-14.56 95.18-63.95 95.18-123.2C447.1 57.31 390.7 0 319.1 0C250.2 0 193.7 55.93 192.3 125.4l-153.4-120.3C34.41 1.672 29.19 0 24.03 0C16.91 0 9.845 3.156 5.127 9.187c-8.187 10.44-6.375 25.53 4.062 33.7L601.2 506.9c10.5 8.203 25.56 6.328 33.69-4.078C643.1 492.4 641.2 477.3 630.8 469.1z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-image-slash">
|
||||
<path d="M21 17.2L6.8 3H19C20.1 3 21 3.9 21 5V17.2M20.7 22L19.7 21H5C3.9 21 3 20.1 3 19V4.3L2 3.3L3.3 2L22 20.7L20.7 22M16.8 18L12.9 14.1L11 16.5L8.5 13.5L5 18H16.8Z"></path>
|
||||
</symbol>
|
||||
|
@ -60,14 +37,10 @@
|
|||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-responsive">
|
||||
<path d="M4,6V16H9V12A2,2 0 0,1 11,10H16A2,2 0 0,1 18,12V16H20V6H4M0,20V18H4A2,2 0 0,1 2,16V6A2,2 0 0,1 4,4H20A2,2 0 0,1 22,6V16A2,2 0 0,1 20,18H24V20H18V20C18,21.11 17.1,22 16,22H11A2,2 0 0,1 9,20H9L0,20M11.5,20A0.5,0.5 0 0,0 11,20.5A0.5,0.5 0 0,0 11.5,21A0.5,0.5 0 0,0 12,20.5A0.5,0.5 0 0,0 11.5,20M15.5,20A0.5,0.5 0 0,0 15,20.5A0.5,0.5 0 0,0 15.5,21A0.5,0.5 0 0,0 16,20.5A0.5,0.5 0 0,0 15.5,20M13,20V21H14V20H13M11,12V19H16V12H11Z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-legal">
|
||||
<path d="M12,3C10.73,3 9.6,3.8 9.18,5H3V7H4.95L2,14C1.53,16 3,17 5.5,17C8,17 9.56,16 9,14L6.05,7H9.17C9.5,7.85 10.15,8.5 11,8.83V20H2V22H22V20H13V8.82C13.85,8.5 14.5,7.85 14.82,7H17.95L15,14C14.53,16 16,17 18.5,17C21,17 22.56,16 22,14L19.05,7H21V5H14.83C14.4,3.8 13.27,3 12,3M12,5A1,1 0 0,1 13,6A1,1 0 0,1 12,7A1,1 0 0,1 11,6A1,1 0 0,1 12,5M5.5,10.25L7,14H4L5.5,10.25M18.5,10.25L20,14H17L18.5,10.25Z"></path>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-search">
|
||||
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-code-block">
|
||||
<path d="M5,3H7V5H5V10A2,2 0 0,1 3,12A2,2 0 0,1 5,14V19H7V21H5C3.93,20.73 3,20.1 3,19V15A2,2 0 0,0 1,13H0V11H1A2,2 0 0,0 3,9V5A2,2 0 0,1 5,3M19,3A2,2 0 0,1 21,5V9A2,2 0 0,0 23,11H24V13H23A2,2 0 0,0 21,15V19A2,2 0 0,1 19,21H17V19H19V14A2,2 0 0,1 21,12A2,2 0 0,1 19,10V5H17V3H19M12,15A1,1 0 0,1 13,16A1,1 0 0,1 12,17A1,1 0 0,1 11,16A1,1 0 0,1 12,15M8,15A1,1 0 0,1 9,16A1,1 0 0,1 8,17A1,1 0 0,1 7,16A1,1 0 0,1 8,15M16,15A1,1 0 0,1 17,16A1,1 0 0,1 16,17A1,1 0 0,1 15,16A1,1 0 0,1 16,15Z"></path>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-external-link">
|
||||
<path d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-ads-slash">
|
||||
<path d="M12.2 9L10.2 7H13C14.1 7 15 7.9 15 9V11.8L13 9.8V9H12.2M23 9V7H19C17.9 7 17 7.9 17 9V11C17 12.1 17.9 13 19 13H21V15H18.2L20.2 17H21C22.1 17 23 16.1 23 15V13C23 11.9 22.1 11 21 11H19V9H23M22.1 21.5L20.8 22.8L14.4 16.4C14.1 16.7 13.6 17 13 17H9V10.9L7 8.9V17H5V13H3V17H1V9C1 7.9 1.9 7 3 7H5.1L1.1 3L2.4 1.7L22.1 21.5M5 9H3V11H5V9M13 14.9L11 12.9V15H13V14.9Z"></path>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.5 KiB |
22
src/components/find/Company.tsx
Normal file
22
src/components/find/Company.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Companies } from '../../interfaces/shared/search';
|
||||
import Link from 'next/link';
|
||||
|
||||
import styles from '../../styles/modules/components/find/company.module.scss';
|
||||
|
||||
type Props = {
|
||||
company: Companies[0];
|
||||
};
|
||||
|
||||
const Company = ({ company }: Props) => {
|
||||
return (
|
||||
<li className={styles.company}>
|
||||
<Link href={`name/${company.id}`}>
|
||||
<a className={`heading ${styles.heading}`}>{company.name}</a>
|
||||
</Link>
|
||||
{company.country && <p>{company.country}</p>}
|
||||
{!!company.type && <p>{company.type}</p>}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Company;
|
21
src/components/find/Keyword.tsx
Normal file
21
src/components/find/Keyword.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Keywords } from '../../interfaces/shared/search';
|
||||
import Link from 'next/link';
|
||||
|
||||
import styles from '../../styles/modules/components/find/keyword.module.scss';
|
||||
|
||||
type Props = {
|
||||
keyword: Keywords[0];
|
||||
};
|
||||
|
||||
const Keyword = ({ keyword }: Props) => {
|
||||
return (
|
||||
<li className={styles.keyword}>
|
||||
<Link href={`name/${keyword.id}`}>
|
||||
<a className={`heading ${styles.heading}`}>{keyword.text}</a>
|
||||
</Link>
|
||||
{keyword.numTitles && <p>{keyword.numTitles} titles</p>}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Keyword;
|
45
src/components/find/Person.tsx
Normal file
45
src/components/find/Person.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { People } from '../../interfaces/shared/search';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { modifyIMDbImg } from '../../utils/helpers';
|
||||
import styles from '../../styles/modules/components/find/person.module.scss';
|
||||
|
||||
type Props = {
|
||||
person: People[0];
|
||||
};
|
||||
|
||||
const Person = ({ person }: Props) => {
|
||||
return (
|
||||
<li className={styles.person}>
|
||||
<div className={styles.imgContainer} style={{ position: 'relative' }}>
|
||||
{person.image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(person.image.url, 400)}
|
||||
alt={person.image.caption}
|
||||
fill
|
||||
className={styles.img}
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href="/svg/sprite.svg#icon-image-slash" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<Link href={`name/${person.id}`}>
|
||||
<a className={`heading ${styles.heading}`}>{person.name}</a>
|
||||
</Link>
|
||||
{person.aka && <p>{person.aka}</p>}
|
||||
{person.jobCateogry && <p>{person.jobCateogry}</p>}
|
||||
{(person.knownForTitle || person.knownInYear) && (
|
||||
<ul className={styles.basicInfo} aria-label="quick facts">
|
||||
{person.knownForTitle && <li>{person.knownForTitle}</li>}
|
||||
{person.knownInYear && <li>{person.knownInYear}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Person;
|
60
src/components/find/Title.tsx
Normal file
60
src/components/find/Title.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { Titles } from '../../interfaces/shared/search';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { modifyIMDbImg } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/find/title.module.scss';
|
||||
|
||||
type Props = {
|
||||
title: Titles[0];
|
||||
};
|
||||
|
||||
const Title = ({ title }: Props) => {
|
||||
return (
|
||||
<li className={styles.title}>
|
||||
<div className={styles.imgContainer}>
|
||||
{title.image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(title.image.url, 400)}
|
||||
alt={title.image.caption}
|
||||
fill
|
||||
className={styles.img}
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href="/svg/sprite.svg#icon-image-slash" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<Link href={`/title/${title.id}`}>
|
||||
<a className={`heading ${styles.heading}`}>{title.name}</a>
|
||||
</Link>
|
||||
<ul aria-label="quick facts" className={styles.basicInfo}>
|
||||
{title.type && <li>{title.type}</li>}
|
||||
{title.sAndE && <li>{title.sAndE}</li>}
|
||||
{title.releaseYear && <li>{title.releaseYear}</li>}
|
||||
</ul>
|
||||
{!!title.credits.length && (
|
||||
<p className={styles.stars}>
|
||||
<span>Stars: </span>
|
||||
{title.credits.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
{title.seriesId && (
|
||||
<ul aria-label="quick series facts" className={styles.seriesInfo}>
|
||||
{title.seriesType && <li>{title.seriesType}</li>}
|
||||
<li>
|
||||
<Link href={`/title/${title.seriesId}`}>
|
||||
<a className="link">{title.seriesName}</a>
|
||||
</Link>
|
||||
</li>
|
||||
{title.seriesReleaseYear && <li>{title.seriesReleaseYear}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Title;
|
95
src/components/find/index.tsx
Normal file
95
src/components/find/index.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import Find from '../../interfaces/shared/search';
|
||||
import Company from './Company';
|
||||
import Person from './Person';
|
||||
import Title from './Title';
|
||||
|
||||
import styles from '../../styles/modules/components/find/results.module.scss';
|
||||
import Keyword from './Keyword';
|
||||
import { getResTitleTypeHeading } from '../../utils/helpers';
|
||||
|
||||
type Props = {
|
||||
results: Find | null;
|
||||
className?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const resultsExist = (results: Props['results']) => {
|
||||
if (
|
||||
!results ||
|
||||
(!results.people.length &&
|
||||
!results.keywords.length &&
|
||||
!results.companies.length &&
|
||||
!results.titles.length)
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// MAIN COMPONENT
|
||||
const Results = ({ results, className, title }: Props) => {
|
||||
if (!resultsExist(results))
|
||||
return (
|
||||
<h1 className={`heading heading__primary ${className}`}>
|
||||
No results found
|
||||
</h1>
|
||||
);
|
||||
|
||||
const { titles, people, keywords, companies, meta } = results!;
|
||||
const titlesSectionHeading = getResTitleTypeHeading(
|
||||
meta.type,
|
||||
meta.titleType
|
||||
);
|
||||
|
||||
return (
|
||||
<article className={`${className} ${styles.results}`}>
|
||||
<h1 className="heading heading__primary">Results for '{title}'</h1>
|
||||
<div className={styles.results__list}>
|
||||
{!!titles.length && (
|
||||
<section className={styles.titles}>
|
||||
<h2 className="heading heading__secondary">
|
||||
{titlesSectionHeading}
|
||||
</h2>
|
||||
<ul className={styles.titles__list}>
|
||||
{titles.map(title => (
|
||||
<Title title={title} key={title.id} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!people.length && (
|
||||
<section className={styles.people}>
|
||||
<h2 className="heading heading__secondary">People</h2>
|
||||
<ul className={styles.people__list}>
|
||||
{people.map(person => (
|
||||
<Person person={person} key={person.id} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!companies.length && (
|
||||
<section className={styles.people}>
|
||||
<h2 className="heading heading__secondary">Companies</h2>
|
||||
<ul className={styles.people__list}>
|
||||
{companies.map(company => (
|
||||
<Company company={company} key={company.id} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!keywords.length && (
|
||||
<section className={styles.people}>
|
||||
<h2 className="heading heading__secondary">Keywords</h2>
|
||||
<ul className={styles.people__list}>
|
||||
{keywords.map(keyword => (
|
||||
<Keyword keyword={keyword} key={keyword.id} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
export default Results;
|
124
src/components/forms/find/index.tsx
Normal file
124
src/components/forms/find/index.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import { ChangeEventHandler, FormEventHandler, useRef, useState } from 'react';
|
||||
import { cleanQueryStr } from '../../../utils/helpers';
|
||||
import { resultTypes, resultTitleTypes } from '../../../utils/constants/find';
|
||||
|
||||
import styles from '../../../styles/modules/components/form/find.module.scss';
|
||||
import { QueryTypes } from '../../../interfaces/shared/search';
|
||||
|
||||
/**
|
||||
* helper function to render similar radio btns. saves from boilerplate.
|
||||
* @param data radio btn obj
|
||||
* @param parentClass class under which radio input and label will be
|
||||
* @returns JSX array of radios
|
||||
*/
|
||||
const renderRadioBtns = (
|
||||
data: typeof resultTypes | typeof resultTitleTypes,
|
||||
parentClass: string
|
||||
) => {
|
||||
return data.types.map(({ name, val }) => (
|
||||
<p className={parentClass} key={val}>
|
||||
<input
|
||||
type="radio"
|
||||
name={data.key}
|
||||
id={`${data.key}:${val}`}
|
||||
value={val}
|
||||
className="visually-hidden"
|
||||
/>
|
||||
<label htmlFor={`${data.key}:${val}`}>{name}</label>
|
||||
</p>
|
||||
));
|
||||
};
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// MAIN FUNCTION
|
||||
const Form = ({ className }: Props) => {
|
||||
const router = useRouter();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
|
||||
// title types can't be selected unless type selected is 'title'. below is the logic for disabling/enabling titleTypes.
|
||||
const typesChangeHandler: ChangeEventHandler<HTMLFieldSetElement> = e => {
|
||||
const el = e.target as unknown as HTMLInputElement; // we have only radios that'll fire change event.
|
||||
const value = el.value as QueryTypes;
|
||||
|
||||
if (value === 'tt') setIsDisabled(false);
|
||||
else setIsDisabled(true);
|
||||
};
|
||||
|
||||
// preventing page refresh and instead handling submission through js
|
||||
const submitHandler: FormEventHandler<HTMLFormElement> = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const formEl = formRef.current!;
|
||||
const formData = new FormData(formEl);
|
||||
const query = (formData.get('q') as string).trim();
|
||||
|
||||
const entries = [...formData.entries()] as [string, string][];
|
||||
const queryStr = cleanQueryStr(entries);
|
||||
|
||||
if (query) router.push(`/find?${queryStr}`);
|
||||
formEl.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
action="/find"
|
||||
onSubmit={submitHandler}
|
||||
ref={formRef}
|
||||
className={`${className} ${styles.form}`}
|
||||
>
|
||||
<p className="heading heading__primary">Search</p>
|
||||
|
||||
<p className={styles.searchbar}>
|
||||
<svg
|
||||
className={`icon ${styles.searchbar__icon}`}
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
>
|
||||
<use href="/svg/sprite.svg#icon-search"></use>
|
||||
</svg>
|
||||
<input
|
||||
id="searchbar"
|
||||
type="search"
|
||||
name="q"
|
||||
placeholder="movies, people..."
|
||||
className={styles.searchbar__input}
|
||||
/>
|
||||
<label className="visually-hidden" htmlFor="searchbar">
|
||||
Search for anything
|
||||
</label>
|
||||
</p>
|
||||
<fieldset className={styles.types} onChange={typesChangeHandler}>
|
||||
<legend className={`heading ${styles.types__heading}`}>
|
||||
Filter by Type
|
||||
</legend>
|
||||
{renderRadioBtns(resultTypes, styles.type)}
|
||||
</fieldset>
|
||||
<fieldset className={styles.titleTypes} disabled={isDisabled}>
|
||||
<legend className={`heading ${styles.titleTypes__heading}`}>
|
||||
Filter by Title Type
|
||||
</legend>
|
||||
{renderRadioBtns(resultTitleTypes, styles.titleType)}
|
||||
</fieldset>
|
||||
<p className={styles.exact}>
|
||||
<label htmlFor="exact">Exact Matches</label>
|
||||
<input type="checkbox" name="exact" id="exact" value="true" />
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<button type="reset" className={styles.button}>
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" className={styles.button}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form;
|
83
src/interfaces/misc/rawFind.ts
Normal file
83
src/interfaces/misc/rawFind.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { ResultMetaTitleTypes, ResultMetaTypes } from '../shared/search';
|
||||
|
||||
export default interface RawFind {
|
||||
props: {
|
||||
pageProps: {
|
||||
findPageMeta: {
|
||||
searchTerm: string;
|
||||
includeAdult: false;
|
||||
isExactMatch: boolean;
|
||||
searchType?: ResultMetaTypes;
|
||||
titleSearchType?: ResultMetaTitleTypes[];
|
||||
};
|
||||
nameResults: {
|
||||
results: Array<{
|
||||
id: string;
|
||||
displayNameText: string;
|
||||
knownForJobCategory: string | 0;
|
||||
knownForTitleText: string | 0;
|
||||
knownForTitleYear: string | 0;
|
||||
avatarImageModel?: {
|
||||
url: string;
|
||||
// maxHeight: number;
|
||||
// maxWidth: number;
|
||||
caption: string;
|
||||
};
|
||||
akaName?: string;
|
||||
}>;
|
||||
// nextCursor?: string;
|
||||
// hasExactMatches?: boolean;
|
||||
};
|
||||
titleResults: {
|
||||
results: Array<{
|
||||
id: string;
|
||||
titleNameText: string;
|
||||
titleReleaseText?: string;
|
||||
titleTypeText: string;
|
||||
titlePosterImageModel?: {
|
||||
url: string;
|
||||
// maxHeight: number;
|
||||
// maxWidth: number;
|
||||
caption: string;
|
||||
};
|
||||
topCredits: Array<string>;
|
||||
imageType: string;
|
||||
seriesId?: string;
|
||||
seriesNameText?: string;
|
||||
seriesReleaseText?: string;
|
||||
seriesTypeText?: string;
|
||||
seriesSeasonText?: string;
|
||||
seriesEpisodeText?: string;
|
||||
}>;
|
||||
// nextCursor?: string;
|
||||
// hasExactMatches?: boolean;
|
||||
};
|
||||
companyResults: {
|
||||
results: Array<{
|
||||
id: string;
|
||||
companyName: string;
|
||||
countryText: string;
|
||||
typeText: string | 0;
|
||||
}>;
|
||||
// nextCursor?: string;
|
||||
// hasExactMatches?: boolean;
|
||||
};
|
||||
keywordResults: {
|
||||
results: Array<{
|
||||
id: string;
|
||||
keywordText: string;
|
||||
numTitles: number;
|
||||
}>;
|
||||
// nextCursor?: string;
|
||||
// hasExactMatches?: boolean;
|
||||
};
|
||||
resultsSectionOrder: Array<string>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// const x: RawFind<'tt'> = {
|
||||
// props: {pageProps: {findPageMeta: {
|
||||
// titleSearchType: ['MOVIE']
|
||||
// }}}
|
||||
// }
|
28
src/interfaces/shared/search.ts
Normal file
28
src/interfaces/shared/search.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import cleanFind from '../../utils/cleaners/find';
|
||||
import { resultTitleTypes, resultTypes } from '../../utils/constants/find';
|
||||
|
||||
type BasicSearch = ReturnType<typeof cleanFind>;
|
||||
export type { BasicSearch as default };
|
||||
|
||||
export type Titles = BasicSearch['titles'];
|
||||
export type People = BasicSearch['people'];
|
||||
export type Companies = BasicSearch['companies'];
|
||||
export type Keywords = BasicSearch['keywords'];
|
||||
|
||||
// q=babylon&s=tt&ttype=ft&exact=true
|
||||
export type FindQueryParams = {
|
||||
q: string;
|
||||
exact?: 'true';
|
||||
s?: QueryTypes;
|
||||
ttype?: QueryTitleTypes;
|
||||
};
|
||||
|
||||
export type ResultMetaTypes = typeof resultTypes.types[number]['id'] | null;
|
||||
|
||||
export type ResultMetaTitleTypes =
|
||||
| typeof resultTitleTypes.types[number]['id']
|
||||
| null;
|
||||
|
||||
export type QueryTypes = typeof resultTypes.types[number]['val'];
|
||||
|
||||
export type QueryTitleTypes = typeof resultTitleTypes.types[number]['val'];
|
|
@ -18,6 +18,11 @@ const Footer: FC = () => {
|
|||
<a className={className('/about')}>About</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/find'>
|
||||
<a className={className('/find')}>Search</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/privacy'>
|
||||
<a className={className('/privacy')}>Privacy</a>
|
||||
|
|
|
@ -1,31 +1,24 @@
|
|||
import { ReactNode } from 'react';
|
||||
// import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import styles from '../styles/modules/layout/header.module.scss';
|
||||
import ThemeToggler from '../components/buttons/ThemeToggler';
|
||||
|
||||
// const ThemeToggler = dynamic(
|
||||
// () => import('../components/buttons/ThemeToggler'),
|
||||
// { ssr: false }
|
||||
// );
|
||||
import styles from '../styles/modules/layout/header.module.scss';
|
||||
|
||||
type Props = { full?: boolean; children?: ReactNode };
|
||||
|
||||
const Header = (props: Props) => {
|
||||
const { asPath: path } = useRouter();
|
||||
|
||||
return (
|
||||
<header
|
||||
id='header'
|
||||
className={`${styles.header} ${props.full ? styles.header__about : ''}`}
|
||||
>
|
||||
<div className={styles.topbar}>
|
||||
<Link href='/about'>
|
||||
<Link href='/'>
|
||||
<a aria-label='go to homepage' className={styles.logo}>
|
||||
<svg
|
||||
className={styles.logo__icon}
|
||||
focusable='false'
|
||||
role='img'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<svg className={styles.logo__icon} role='img' aria-hidden>
|
||||
<use href='/svg/sprite.svg#icon-logo'></use>
|
||||
</svg>
|
||||
<span className={styles.logo__text}>libremdb</span>
|
||||
|
@ -52,23 +45,45 @@ const Header = (props: Props) => {
|
|||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
<div className={styles.misc}>
|
||||
<a
|
||||
href={`https://www.imdb.com${path}`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
<span className='visually-hidden'>
|
||||
View on IMDb (opens in new tab)
|
||||
</span>
|
||||
<svg className='icon' role='img' aria-hidden>
|
||||
<use href='/svg/sprite.svg#icon-external-link'></use>
|
||||
</svg>
|
||||
</a>
|
||||
<Link href='/find'>
|
||||
<a>
|
||||
<span className='visually-hidden'>Search</span>
|
||||
<svg className='icon' role='img' aria-hidden>
|
||||
<use href='/svg/sprite.svg#icon-search'></use>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
<ThemeToggler className={styles.themeToggler} />
|
||||
</div>
|
||||
</div>
|
||||
{props.full && (
|
||||
<div className={styles.hero}>
|
||||
<h1 className={`heading heading__primary ${styles.hero__text}`}>
|
||||
A free & open source IMDb front-end
|
||||
</h1>
|
||||
<p className={styles.hero__more}>
|
||||
inspired by projects like
|
||||
inspired by projects like{' '}
|
||||
<a href='https://codeberg.org/teddit/teddit' className='link'>
|
||||
teddit
|
||||
</a>
|
||||
,
|
||||
,{' '}
|
||||
<a href='https://github.com/zedeus/nitter' className='link'>
|
||||
nitter
|
||||
</a>
|
||||
, and
|
||||
, and{' '}
|
||||
<a
|
||||
href='https://github.com/digitalblossom/alternative-frontends'
|
||||
className='link'
|
||||
|
|
77
src/pages/find/index.tsx
Normal file
77
src/pages/find/index.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { GetServerSideProps } from 'next';
|
||||
|
||||
import Layout from '../../layouts/Layout';
|
||||
import ErrorInfo from '../../components/error/ErrorInfo';
|
||||
import Meta from '../../components/meta/Meta';
|
||||
import Results from '../../components/find';
|
||||
import basicSearch from '../../utils/fetchers/basicSearch';
|
||||
import Form from '../../components/forms/find';
|
||||
|
||||
import Find, { FindQueryParams } from '../../interfaces/shared/search';
|
||||
import { AppError } from '../../interfaces/shared/error';
|
||||
import { cleanQueryStr } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/pages/find/find.module.scss';
|
||||
|
||||
type Props =
|
||||
| { data: { title: string; results: Find }; error: null }
|
||||
| { data: { title: null; results: null }; error: null }
|
||||
| { data: { title: string; results: null }; error: AppError };
|
||||
|
||||
const getMetadata = (title: string | null) => ({
|
||||
title: title || 'Search',
|
||||
description: title
|
||||
? `results for '${title}'`
|
||||
: 'Search for anything on libremdb, a free & open source IMDb front-end',
|
||||
});
|
||||
|
||||
const BasicSearch = ({ data: { title, results }, error }: Props) => {
|
||||
if (error)
|
||||
return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta {...getMetadata(title)} />
|
||||
<Layout className={`${styles.find} ${!title && styles.find__home}`}>
|
||||
{title && ( // only showing when user has searched for something
|
||||
<Results results={results} title={title} className={styles.results} />
|
||||
)}
|
||||
<Form className={styles.form} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: use generics for passing in queryParams(to components) for better type-checking.
|
||||
export const getServerSideProps: GetServerSideProps = async ctx => {
|
||||
// sample query str: find/?q=babylon&s=tt&ttype=ft&exact=true
|
||||
const queryObj = ctx.query as FindQueryParams;
|
||||
const query = queryObj.q?.trim();
|
||||
|
||||
if (!query)
|
||||
return { props: { data: { title: null, results: null }, error: null } };
|
||||
|
||||
try {
|
||||
const entries = Object.entries(queryObj);
|
||||
const queryStr = cleanQueryStr(entries);
|
||||
|
||||
const res = await basicSearch(queryStr);
|
||||
|
||||
return {
|
||||
props: { data: { title: query, results: res }, error: null },
|
||||
};
|
||||
} catch (error: any) {
|
||||
const { message, statusCode } = error;
|
||||
ctx.res.statusCode = statusCode;
|
||||
ctx.res.statusMessage = message;
|
||||
|
||||
return {
|
||||
props: {
|
||||
error: { message, statusCode },
|
||||
data: { title: query, results: null },
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default BasicSearch;
|
13
src/styles/modules/components/find/company.module.scss
Normal file
13
src/styles/modules/components/find/company.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.company {
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
display: grid;
|
||||
padding: var(--spacer-3);
|
||||
gap: var(--spacer-0);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
13
src/styles/modules/components/find/keyword.module.scss
Normal file
13
src/styles/modules/components/find/keyword.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.keyword {
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
display: grid;
|
||||
padding: var(--spacer-3);
|
||||
gap: var(--spacer-0);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
74
src/styles/modules/components/find/person.module.scss
Normal file
74
src/styles/modules/components/find/person.module.scss
Normal file
|
@ -0,0 +1,74 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.person {
|
||||
--width: 10rem;
|
||||
--height: var(--width);
|
||||
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
overflow: hidden; // for background image
|
||||
display: grid;
|
||||
grid-template-columns: var(--width) auto;
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
--height: 15rem;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: var(--height);
|
||||
}
|
||||
|
||||
.img {
|
||||
object-fit: cover;
|
||||
object-position: center 25%; // most of the time, person's face is visible at 1/4 of height in a potrait image.
|
||||
|
||||
}
|
||||
|
||||
.imgNA {
|
||||
width: 80%;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
padding: var(--spacer-3);
|
||||
gap: var(--spacer-0);
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
padding: var(--spacer-1);
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.basicInfo, .seriesInfo {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& * + ::before {
|
||||
content: '\00b7';
|
||||
padding-inline: var(--spacer-1);
|
||||
font-weight: 900;
|
||||
line-height: 0;
|
||||
font-size: var(--fs-5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.stars {
|
||||
|
||||
span {
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
}
|
25
src/styles/modules/components/find/results.module.scss
Normal file
25
src/styles/modules/components/find/results.module.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.results {
|
||||
display: grid;
|
||||
gap: var(--spacer-2);
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
gap: var(--spacer-5);
|
||||
}
|
||||
}
|
||||
|
||||
.titles, .people, .companies, .keywords {
|
||||
display: grid;
|
||||
gap: var(--spacer-2);
|
||||
|
||||
&__list {
|
||||
padding: var(--spacer-2);
|
||||
display: grid;
|
||||
gap: var(--spacer-4);
|
||||
// justify-self: start;
|
||||
|
||||
|
||||
}
|
||||
}
|
75
src/styles/modules/components/find/title.module.scss
Normal file
75
src/styles/modules/components/find/title.module.scss
Normal file
|
@ -0,0 +1,75 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.title {
|
||||
--width: 10rem;
|
||||
--height: 10rem;
|
||||
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
overflow: hidden; // for background image
|
||||
display: grid;
|
||||
grid-template-columns: var(--width) auto;
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
--height: 15rem;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
min-height: var(--height);
|
||||
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.img {
|
||||
object-fit: cover;
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
object-position: center 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.imgNA {
|
||||
width: 80%;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: grid;
|
||||
gap: var(--spacer-0);
|
||||
padding: var(--spacer-3);
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
padding: var(--spacer-1);
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: var(--fs-4);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.basicInfo,
|
||||
.seriesInfo {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& * + ::before {
|
||||
content: '\00b7';
|
||||
padding-inline: var(--spacer-1);
|
||||
font-weight: 900;
|
||||
line-height: 0;
|
||||
font-size: var(--fs-5);
|
||||
}
|
||||
}
|
||||
|
||||
.stars {
|
||||
span {
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
}
|
148
src/styles/modules/components/form/find.module.scss
Normal file
148
src/styles/modules/components/form/find.module.scss
Normal file
|
@ -0,0 +1,148 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--spacer-2);
|
||||
|
||||
position: sticky;
|
||||
top: var(--spacer-2);
|
||||
|
||||
@include helper.bp('bp-1200') {
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
|
||||
%border-styles {
|
||||
border-radius: var(--spacer-1);
|
||||
border: 2px solid var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
.searchbar {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: var(--spacer-1);
|
||||
padding: var(--spacer-1);
|
||||
|
||||
@extend %border-styles;
|
||||
|
||||
--dim: 3rem;
|
||||
|
||||
&__icon {
|
||||
height: var(--dim);
|
||||
width: var(--dim);
|
||||
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
&__input {
|
||||
font: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
caret-color: var(--clr-fill);
|
||||
background: transparent;
|
||||
color: var(--clr-text-accent);
|
||||
|
||||
-webkit-appearance: none; // webkit sucks!
|
||||
}
|
||||
|
||||
// accessibility
|
||||
&:focus-within {
|
||||
background: var(--clr-bg-muted);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.types,
|
||||
.titleTypes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacer-2);
|
||||
padding: var(--spacer-2);
|
||||
|
||||
@extend %border-styles;
|
||||
|
||||
&__heading {
|
||||
font-size: var(--fs-4);
|
||||
padding-inline: var(--spacer-1);
|
||||
flex: 100%;
|
||||
line-height: 1;
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&, * {
|
||||
cursor: not-allowed;
|
||||
filter: brightness(.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.type,
|
||||
.titleType {
|
||||
--border-color: transparent;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
padding: var(--spacer-1) var(--spacer-2);
|
||||
border-radius: 5px;
|
||||
color: var(--clr-text-accent);
|
||||
background-color: var(--clr-bg-accent);
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
input:checked + label {
|
||||
--border-color: var(--clr-text-accent);
|
||||
}
|
||||
|
||||
// for keyboard navigation
|
||||
input:focus + label {
|
||||
@include helper.focus-rules;
|
||||
}
|
||||
|
||||
@supports selector(:focus-visible) {
|
||||
input:focus + label {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus-visible + label {
|
||||
@include helper.focus-rules;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.exact {
|
||||
display: flex;
|
||||
gap: var(--spacer-1);
|
||||
justify-self: start;
|
||||
align-items: center;
|
||||
|
||||
label, input {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: var(--spacer-2);
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
--text: var(--clr-link);
|
||||
|
||||
padding: var(--spacer-1) var(--spacer-2);
|
||||
font: inherit;
|
||||
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 2px solid currentColor;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
&[type='reset'] {
|
||||
--text: var(--clr-text-muted)
|
||||
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
@use '../../abstracts' as helper;
|
||||
|
||||
.header {
|
||||
--dimension: 1.5em; // will be used for icons
|
||||
--dimension: 1.6em; // will be used for icons
|
||||
|
||||
font-size: 1.1em;
|
||||
|
||||
|
@ -67,9 +67,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.themeToggler {
|
||||
.misc {
|
||||
justify-self: end;
|
||||
grid-column: -2 / -1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacer-2);
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
height: var(--dimension);
|
||||
width: var(--dimension);
|
||||
fill: var(--clr-fill);
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
|
|
44
src/styles/modules/pages/find/find.module.scss
Normal file
44
src/styles/modules/pages/find/find.module.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.find {
|
||||
// major whitespace properties used on title page
|
||||
--doc-whitespace: var(--spacer-8);
|
||||
--comp-whitespace: var(--spacer-3);
|
||||
|
||||
display: grid;
|
||||
|
||||
gap: var(--doc-whitespace);
|
||||
padding: var(--doc-whitespace);
|
||||
align-items: start;
|
||||
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-areas: 'results results results form form';
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
grid-template-columns: none;
|
||||
grid-template-areas: 'results' 'form';
|
||||
}
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
--doc-whitespace: var(--spacer-5);
|
||||
}
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
padding: var(--spacer-3);
|
||||
}
|
||||
|
||||
&__home {
|
||||
grid-template-columns: unset;
|
||||
grid-template-areas: 'form';
|
||||
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
grid-area: results;
|
||||
}
|
||||
|
||||
.form {
|
||||
grid-area: form;
|
||||
}
|
71
src/utils/cleaners/find.ts
Normal file
71
src/utils/cleaners/find.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import RawFind from '../../interfaces/misc/rawFind';
|
||||
|
||||
const formatSAndE = (
|
||||
season: string | undefined,
|
||||
episode: string | undefined
|
||||
) => {
|
||||
if (season && season !== 'Unknown' && episode && episode !== 'Unknown')
|
||||
return `S${season} E${episode}`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const cleanFind = (rawFind: RawFind) => {
|
||||
const {
|
||||
props: { pageProps: d },
|
||||
} = rawFind;
|
||||
|
||||
const cleanData = {
|
||||
meta: {
|
||||
exact: d.findPageMeta.isExactMatch,
|
||||
type: d.findPageMeta.searchType || null,
|
||||
titleType: d.findPageMeta.titleSearchType?.[0] || null,
|
||||
},
|
||||
people: d.nameResults.results.map(person => ({
|
||||
id: person.id,
|
||||
name: person.displayNameText,
|
||||
aka: person.akaName || null,
|
||||
jobCateogry: person.knownForJobCategory || null,
|
||||
knownForTitle: person.knownForTitleText || null,
|
||||
knownInYear: person.knownForTitleYear || null,
|
||||
...(person.avatarImageModel && {
|
||||
image: {
|
||||
url: person.avatarImageModel.url,
|
||||
caption: person.avatarImageModel.caption,
|
||||
},
|
||||
}),
|
||||
})),
|
||||
titles: d.titleResults.results.map(title => ({
|
||||
id: title.id,
|
||||
name: title.titleNameText,
|
||||
type: title.titleTypeText,
|
||||
releaseYear: title.titleReleaseText || null,
|
||||
credits: title.topCredits,
|
||||
...(title.titlePosterImageModel && {
|
||||
image: {
|
||||
url: title.titlePosterImageModel.url,
|
||||
caption: title.titlePosterImageModel.caption,
|
||||
},
|
||||
}),
|
||||
seriesId: title.seriesId || null,
|
||||
seriesName: title.seriesNameText || null,
|
||||
seriesType: title.seriesTypeText || null,
|
||||
seriesReleaseYear: title.seriesReleaseText || null,
|
||||
sAndE: formatSAndE(title.seriesSeasonText, title.seriesEpisodeText),
|
||||
})),
|
||||
companies: d.companyResults.results.map(company => ({
|
||||
id: company.id,
|
||||
name: company.companyName,
|
||||
type: company.typeText,
|
||||
country: company.countryText,
|
||||
})),
|
||||
keywords: d.keywordResults.results.map(keyword => ({
|
||||
id: keyword.id,
|
||||
text: keyword.keywordText,
|
||||
numTitles: keyword.numTitles,
|
||||
})),
|
||||
};
|
||||
|
||||
return cleanData;
|
||||
};
|
||||
|
||||
export default cleanFind;
|
36
src/utils/constants/find.ts
Normal file
36
src/utils/constants/find.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* @constant
|
||||
*
|
||||
* key: the key for the query that we make to fetch results
|
||||
*
|
||||
* name: Nice name to display on the client side
|
||||
*
|
||||
* val: the value that is associated with the key. also used to fetch results.
|
||||
*
|
||||
* **IMPORTANT**: see sample response from backend, and form submission url to better understand how these objects are used.
|
||||
*/
|
||||
export const resultTypes = {
|
||||
types: [
|
||||
{ name: 'Titles', val: 'tt', id: 'TITLE' },
|
||||
{ name: 'People', val: 'nm', id: 'NAME' },
|
||||
{ name: 'Companies', val: 'co', id: 'COMPANY' },
|
||||
{ name: 'Keywords', val: 'kw', id: 'KEYWORD' },
|
||||
],
|
||||
key: 's',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* same as {@link resultTypes}.
|
||||
*/
|
||||
export const resultTitleTypes = {
|
||||
types: [
|
||||
{ name: 'Movies', val: 'ft', id: 'MOVIE' },
|
||||
{ name: 'TV', val: 'tv', id: 'TV' },
|
||||
{ name: 'TV Episodes', val: 'ep', id: 'TV_EPISODE' },
|
||||
{ name: 'Music Videos', val: 'mu', id: 'MUSIC_VIDEO' },
|
||||
{ name: 'Podcasts', val: 'ps', id: 'PODCAST_SERIES' },
|
||||
{ name: 'Podcast Episodes', val: 'pe', id: 'PODCAST_EPISODE' },
|
||||
{ name: 'Video Games', val: 'vg', id: 'VIDEO_GAME' },
|
||||
],
|
||||
key: 'ttype',
|
||||
} as const;
|
27
src/utils/fetchers/basicSearch.ts
Normal file
27
src/utils/fetchers/basicSearch.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
// external deps
|
||||
import * as cheerio from 'cheerio';
|
||||
// local files
|
||||
import axiosInstance from '../axiosInstance';
|
||||
import { AppError } from '../helpers';
|
||||
import RawFind from '../../interfaces/misc/rawFind';
|
||||
import cleanFind from '../cleaners/find';
|
||||
|
||||
const basicSearch = async (queryStr: string) => {
|
||||
try {
|
||||
const res = await axiosInstance(`/find?${queryStr}`);
|
||||
const $ = cheerio.load(res.data);
|
||||
const rawData = $('script#__NEXT_DATA__').text();
|
||||
|
||||
const parsedRawData: RawFind = JSON.parse(rawData);
|
||||
const cleanData = cleanFind(parsedRawData);
|
||||
|
||||
return cleanData;
|
||||
} catch (err: any) {
|
||||
if (err.response?.status === 404)
|
||||
throw new AppError('not found', 404, err.cause);
|
||||
|
||||
throw new AppError('something went wrong', 500, err.cause);
|
||||
}
|
||||
};
|
||||
|
||||
export default basicSearch;
|
|
@ -1,3 +1,9 @@
|
|||
import {
|
||||
ResultMetaTitleTypes,
|
||||
ResultMetaTypes,
|
||||
} from '../interfaces/shared/search';
|
||||
import { resultTitleTypes } from './constants/find';
|
||||
|
||||
export const formatTime = (timeInSecs: number) => {
|
||||
if (!timeInSecs) return;
|
||||
// year, month, date, hours, minutes, seconds
|
||||
|
@ -50,8 +56,14 @@ export const formatMoney = (num: number, cur: string) => {
|
|||
}).format(num);
|
||||
};
|
||||
|
||||
const imageRegex = /https:\/\/m\.media-amazon\.com\/images\/M\/[^.]*/;
|
||||
|
||||
export const modifyIMDbImg = (url: string, widthInPx = 600) => {
|
||||
return url.replace(/\.jpg/g, `UX${widthInPx}.jpg`);
|
||||
// as match returns either array or null, returning array in case it returns null. and destructuring it right away.
|
||||
const [cleanImg] = url.match(imageRegex) || [];
|
||||
|
||||
if (cleanImg) return `${cleanImg}.UX${widthInPx}.jpg`;
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getProxiedIMDbImgUrl = (url: string) => {
|
||||
|
@ -65,3 +77,29 @@ export const AppError = class extends Error {
|
|||
Error.captureStackTrace(this, AppError);
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanQueryStr = (
|
||||
entries: [string, string][],
|
||||
filterable = ['q', 's', 'exact', 'ttype']
|
||||
) => {
|
||||
let queryStr = '';
|
||||
|
||||
entries.forEach(([key, val], i) => {
|
||||
if (!val || !filterable.includes(key)) return;
|
||||
queryStr += `${i > 0 ? '&' : ''}${key}=${val.trim()}`;
|
||||
});
|
||||
|
||||
return queryStr;
|
||||
};
|
||||
|
||||
export const getResTitleTypeHeading = (
|
||||
type: ResultMetaTypes,
|
||||
titleType: ResultMetaTitleTypes
|
||||
) => {
|
||||
if (type !== 'TITLE') return 'Titles';
|
||||
|
||||
for (let i = 0; i < resultTitleTypes.types.length; i++) {
|
||||
const el = resultTitleTypes.types[i];
|
||||
if (el.id === titleType) return el.name;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue