Skip to content

Commit dc42b32

Browse files
committed
feat(reviews): add reviews route
allows seeing all reviews with optional filters. works sans JS as well closes #25 closes https://codeberg.org/zyachel/libremdb/issues/19
1 parent cf71cd3 commit dc42b32

File tree

29 files changed

+1053
-31
lines changed

29 files changed

+1053
-31
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,15 @@ Instances list in JSON format can be found in [instances.json](instances.json) f
101101
- [x] add a way to see trailer and other videos
102102
- [ ] implement movie specific routes like:
103103

104-
- [ ] reviews(including critic reviews)
104+
- [x] reviews(including critic reviews)
105105
- [ ] video & image gallery
106106
- [ ] sections under 'did you know'
107107
- [ ] release info
108108
- [ ] parental guide
109109

110110
- [ ] implement other routes like:
111111

112-
- [ ] lists
112+
- [x] lists
113113
- [ ] moviemeter
114114
- [x] person info(includes directors and actors)
115115
- [ ] company info

public/svg/sprite.svg

Lines changed: 6 additions & 0 deletions
Loading

src/components/card/CardResult.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1-
import { ComponentPropsWithoutRef, ReactNode } from 'react';
1+
import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';
22
import Link from 'next/link';
33
import Image from 'next/future/image';
44
import Card from './Card';
55
import { modifyIMDbImg } from 'src/utils/helpers';
66
import styles from 'src/styles/modules/components/card/card-result.module.scss';
77

8-
type Props = {
8+
type Props<T extends ElementType> = {
99
link: string;
1010
name: string;
1111
image?: string;
1212
showImage?: true;
1313
children?: ReactNode;
14-
} & ComponentPropsWithoutRef<'li'>;
14+
as?: T;
15+
} & ComponentPropsWithoutRef<T>;
1516

16-
const CardResult = ({ link, name, image, showImage, children, ...rest }: Props) => {
17+
const CardResult = <T extends 'li' | 'section' | 'div' = 'li'>({
18+
link,
19+
name,
20+
image,
21+
showImage,
22+
className,
23+
children,
24+
...rest
25+
}: Props<T>) => {
1726
let ImageComponent = null;
1827
if (showImage)
1928
ImageComponent = image ? (
@@ -25,7 +34,11 @@ const CardResult = ({ link, name, image, showImage, children, ...rest }: Props)
2534
);
2635

2736
return (
28-
<Card hoverable {...rest} className={`${styles.item} ${!showImage && styles.sansImage}`}>
37+
<Card
38+
hoverable
39+
{...rest}
40+
className={`${styles.item} ${!showImage && styles.sansImage} ${className}`}
41+
>
2942
<div className={styles.imgContainer}>{ImageComponent}</div>
3043
<div className={styles.info}>
3144
<Link href={link}>

src/components/forms/find/index.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ChangeEventHandler, FormEventHandler, useRef, useState } from 'react';
22
import { useRouter } from 'next/router';
33
import { cleanQueryStr } from 'src/utils/helpers';
44
import { QueryTypes } from 'src/interfaces/shared/search';
5-
import { resultTypes, resultTitleTypes } from 'src/utils/constants/find';
5+
import { resultTypes, resultTitleTypes, findFilterable } from 'src/utils/constants/find';
66
import styles from 'src/styles/modules/components/form/find.module.scss';
77

88
type Props = {
@@ -29,13 +29,15 @@ const Form = ({ className }: Props) => {
2929

3030
const formEl = formRef.current!;
3131
const formData = new FormData(formEl);
32-
const query = (formData.get('q') as string).trim();
32+
const query = formData.get('q');
33+
if (typeof query !== 'string' || !query.trim()) return setIsDisabled(false);
3334

34-
const entries = [...formData.entries()] as [string, string][];
35-
const queryStr = cleanQueryStr(entries);
35+
const queryParams = Object.fromEntries(
36+
formData.entries() as IterableIterator<[string, string]>
37+
);
38+
const queryStr = cleanQueryStr(queryParams, findFilterable);
3639

37-
if (query) router.push(`/find?${queryStr}`);
38-
else setIsDisabled(false);
40+
router.push(`/find?${queryStr}`);
3941
formEl.reset();
4042
};
4143

@@ -117,5 +119,4 @@ const RadioBtns = ({
117119
</>
118120
);
119121

120-
121122
export default Form;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useRouter } from 'next/router';
2+
import { CardResult } from 'src/components/card';
3+
import TitleReviews, { TitleReviewsCursored } from 'src/interfaces/shared/titleReviews';
4+
5+
type Props = {
6+
meta: TitleReviews['meta'];
7+
className?: string;
8+
};
9+
10+
const Card = ({ meta, className }: Props) => {
11+
return (
12+
<CardResult
13+
as='div'
14+
showImage
15+
name={`${meta.title} ${meta.year}`}
16+
link={`/title/${meta.titleId}`}
17+
image={meta.image ?? undefined}
18+
className={className}
19+
>
20+
<h1 className='heading heading__primary'>User Reviews</h1>
21+
<p>{meta.numReviews}</p>
22+
</CardResult>
23+
);
24+
};
25+
26+
type BasicCardProps = {
27+
meta: TitleReviewsCursored['meta'];
28+
className?: string;
29+
};
30+
export const BasicCard = ({ meta, className }: BasicCardProps) => {
31+
const { titleId } = useRouter().query;
32+
33+
return (
34+
<CardResult
35+
as='div'
36+
showImage
37+
name={meta.title ?? ''}
38+
link={`/title/${titleId}`}
39+
className={className}
40+
>
41+
<h1 className='heading heading__primary'>User Reviews</h1>
42+
</CardResult>
43+
);
44+
};
45+
46+
export default Card;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { FormEventHandler, useRef } from 'react';
2+
import { useRouter } from 'next/router';
3+
import { cleanQueryStr } from 'src/utils/helpers';
4+
import { direction, keys, ratings, sortBy } from 'src/utils/constants/titleReviewsFilters';
5+
import styles from 'src/styles/modules/components/titleReviews/form.module.scss';
6+
7+
type Props = {
8+
className?: string;
9+
titleId: string;
10+
};
11+
12+
const Filters = ({ className, titleId }: Props) => {
13+
const router = useRouter();
14+
const formRef = useRef<HTMLFormElement>(null);
15+
16+
const submitHandler: FormEventHandler<HTMLFormElement> = e => {
17+
e.preventDefault();
18+
19+
const formEl = formRef.current!;
20+
const formData = new FormData(formEl);
21+
22+
const entries = Object.fromEntries(formData.entries()) as Record<string, string>;
23+
const queryStr = cleanQueryStr(entries, keys);
24+
25+
router.push(`/title/${titleId}/reviews?${queryStr}`);
26+
};
27+
28+
return (
29+
<form
30+
action={`/title/${titleId}/reviews`}
31+
onSubmit={submitHandler}
32+
ref={formRef}
33+
className={`${className} ${styles.form}`}
34+
>
35+
<fieldset className={styles.fieldset}>
36+
<legend className={`heading ${styles.fieldset__heading}`}>Filter by Rating</legend>
37+
<RadioBtns data={ratings} className={styles.radio} />
38+
</fieldset>
39+
<fieldset className={styles.fieldset}>
40+
<legend className={`heading ${styles.fieldset__heading}`}>Sort by</legend>
41+
<RadioBtns data={sortBy} className={styles.radio} />
42+
</fieldset>
43+
<fieldset className={styles.fieldset}>
44+
<legend className={`heading ${styles.fieldset__heading}`}>Direction</legend>
45+
<RadioBtns data={direction} className={styles.radio} />
46+
</fieldset>
47+
<p className={styles.exact}>
48+
<label htmlFor='spoiler'>Hide Spoilers</label>
49+
<input type='checkbox' name='spoiler' id='spoiler' value='hide' />
50+
</p>
51+
<div className={styles.buttons}>
52+
<button type='reset' className={styles.button}>
53+
Clear
54+
</button>
55+
<button type='submit' className={styles.button}>
56+
Submit
57+
</button>
58+
</div>
59+
</form>
60+
);
61+
};
62+
63+
const RadioBtns = ({
64+
data,
65+
className,
66+
}: {
67+
data: typeof ratings | typeof sortBy | typeof direction;
68+
className: string;
69+
}) => (
70+
<>
71+
{data.types.map(({ name, val }) => (
72+
<p className={className} key={val}>
73+
<input
74+
type='radio'
75+
name={data.key}
76+
id={`${data.key}:${val}`}
77+
value={val}
78+
className='visually-hidden'
79+
/>
80+
<label htmlFor={`${data.key}:${val}`}>{name}</label>
81+
</p>
82+
))}
83+
</>
84+
);
85+
86+
export default Filters;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import TitleReviews, { TitleReviewsCursored } from 'src/interfaces/shared/titleReviews';
2+
import styles from 'src/styles/modules/components/titleReviews/pagination.module.scss';
3+
import Link from 'next/link';
4+
import { ReactNode } from 'react';
5+
import { useRouter } from 'next/router';
6+
import { cleanQueryStr } from 'src/utils/helpers';
7+
import { direction, keys, ratings, sortBy } from 'src/utils/constants/titleReviewsFilters';
8+
9+
type Props = {
10+
meta: TitleReviewsCursored['meta'];
11+
cursor: string | null;
12+
onClick?: (queryStr: string) => void;
13+
};
14+
15+
const Pagination = ({ cursor, onClick = () => {}, meta }: Props) => {
16+
const router = useRouter();
17+
18+
if (!cursor || !meta.titleId) return null;
19+
const queryParams = router.query as Record<string, string>;
20+
const queryStr = cleanQueryStr(queryParams, keys);
21+
22+
return (
23+
<>
24+
<button className={styles.button} onClick={() => onClick(queryStr)}>
25+
Load More
26+
</button>
27+
<Link href={`/title/${meta.titleId}/reviews/${cursor}?${queryStr}&title=${meta.title ?? ''}`}>
28+
<a className={styles.link}>Load More</a>
29+
</Link>
30+
</>
31+
);
32+
};
33+
34+
export default Pagination;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import TitleReviews from 'src/interfaces/shared/titleReviews';
2+
import styles from 'src/styles/modules/components/titleReviews/reviews.module.scss';
3+
import Link from 'next/link';
4+
import { ReactNode } from 'react';
5+
6+
type Props = {
7+
list: TitleReviews['list'];
8+
className?: string;
9+
children?: ReactNode;
10+
};
11+
12+
const Results = ({ list, className, children }: Props) => {
13+
return (
14+
<section className={className}>
15+
<section className={styles.reviews}>
16+
{list.map(review => (
17+
<Review {...review} key={review.reviewId} />
18+
))}
19+
</section>
20+
{children}
21+
</section>
22+
);
23+
};
24+
25+
const Review = ({
26+
by,
27+
date,
28+
isSpoiler,
29+
rating,
30+
responses,
31+
reviewHtml,
32+
reviewId,
33+
summary,
34+
}: TitleReviews['list'][number]) => {
35+
return (
36+
<article className={styles.reviews__reviewContainer}>
37+
<details className={styles.review}>
38+
<summary className={styles.review__summary}>
39+
<Link href={by.link ?? '#'}>
40+
<a className='link'>{by.name}</a>
41+
</Link>
42+
<time>{date}</time>
43+
<p className={styles.review__misc}>
44+
{isSpoiler && (
45+
<span className={styles.review__misc_spoilers}>
46+
<svg className={styles.icon}>
47+
<use href='/svg/sprite.svg#icon-alert'></use>
48+
</svg>
49+
Spoilers
50+
</span>
51+
)}
52+
{rating && (
53+
<span>
54+
<svg className={styles.icon}>
55+
<use href='/svg/sprite.svg#icon-rating'></use>
56+
</svg>
57+
{rating}
58+
</span>
59+
)}
60+
<svg className={`${styles.icon} ${styles.review__summary_chevron}`}>
61+
<use href='/svg/sprite.svg#icon-chevron'></use>
62+
</svg>
63+
</p>
64+
<strong>{summary}</strong>
65+
</summary>
66+
{reviewHtml && (
67+
<div
68+
className={styles.review__text}
69+
dangerouslySetInnerHTML={{
70+
__html: reviewHtml,
71+
}}
72+
/>
73+
)}
74+
</details>
75+
<footer className={styles.review__metadata}>
76+
<p>{responses}</p>
77+
</footer>
78+
</article>
79+
);
80+
};
81+
82+
export default Results;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { default as Reviews } from './Reviews';
2+
export { default as TitleCard } from './Card';
3+
export * from './Card';
4+
export { default as Filters } from './Filters';
5+
export { default as Pagination } from './Pagination';

src/interfaces/misc/rawName.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ export default interface Name {
562562
};
563563
}>;
564564
totalCredits: {
565-
total: number;
565+
total?: number;
566566
// restriction?: {
567567
// unrestrictedTotal: number;
568568
// explanations: Array<{

0 commit comments

Comments
 (0)