]?: never };
15 |
16 | /**
17 | * @example
18 | * ```ts
19 | * type X = Assoc<["a", "b", "c"], number> // => {a: {b: {c: number } } }
20 | * ```
21 | */
22 | export type Assoc = P extends [
23 | infer F extends string,
24 | ...infer R
25 | ]
26 | ? { [K in F]: Assoc }
27 | : D;
28 |
29 | export type Recur = {
30 | [T in keyof R]: { [S in keyof R[T]]: R[T][S] };
31 | };
32 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | pull_request:
6 | branches: [master]
7 | workflow_dispatch:
8 |
9 | jobs:
10 | test:
11 | name: Run tests
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Set up pnpm
16 | uses: pnpm/action-setup@v2.2.4
17 | with:
18 | version: 8
19 | - name: Set up Node.js
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: 20
23 | cache: "pnpm"
24 | - name: Cache Turborepo Tasks
25 | uses: actions/cache@v3
26 | with:
27 | path: ${{ github.workspace }}/node_modules/.cache/turbo
28 | key: ${{ runner.os }}-turbo-test-${{ hashFiles('**/pnpm-lock.yaml') }}
29 | restore-keys: ${{ runner.os }}-turbo-
30 | - run: pnpm install -rw
31 | - run: pnpm run lint
32 | - run: pnpm run cov
33 |
--------------------------------------------------------------------------------
/apps/web/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches-ignore: [master]
6 | pull_request:
7 | branches: [master]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 | name: Build Next.js App
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: Set up pnpm
17 | uses: pnpm/action-setup@v2.2.4
18 | with:
19 | version: 8
20 | - name: Set up Node.js
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: 20
24 | cache: "pnpm"
25 | - name: Cache Turborepo Tasks
26 | uses: actions/cache@v3
27 | with:
28 | path: ${{ github.workspace }}/node_modules/.cache/turbo
29 | key: ${{ runner.os }}-turbo-build-${{ hashFiles('**/pnpm-lock.yaml') }}
30 | restore-keys: ${{ runner.os }}-turbo-
31 | - run: pnpm install -rw
32 | - run: pnpm run build
33 |
--------------------------------------------------------------------------------
/apps/web/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/apps/web/components/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useToast } from "@/lib/use-toast";
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /** @type {import('prettier').Config} */
4 | /** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */
5 | module.exports = {
6 | importOrder: [
7 | "^(react/(.*)$)|^(react$)",
8 | "^(next/(.*)$)|^(next$)",
9 | "",
10 | "",
11 | "^types$",
12 | "^@/types/(.*)$",
13 | "^@/config/(.*)$",
14 | "^@/lib/(.*)$",
15 | "^@/hooks/(.*)$",
16 | "^@/components/ui/(.*)$",
17 | "^@/components/(.*)$",
18 | "^@/styles/(.*)$",
19 | "^@/app/(.*)$",
20 | "",
21 | "^[./]",
22 | ],
23 | importOrderSortSpecifiers: true,
24 | importOrderBuiltinModulesToTop: true,
25 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
26 | importOrderMergeDuplicateImports: true,
27 | importOrderCombineTypeAndValueImports: true,
28 | plugins: [
29 | "prettier-plugin-tailwindcss",
30 | "@ianvs/prettier-plugin-sort-imports",
31 | ],
32 | };
33 |
--------------------------------------------------------------------------------
/packages/is-plain-object/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # is-plain-object
2 |
3 | ## 0.0.3
4 |
5 | ### Patch Changes
6 |
7 | - [#41](https://github.com/mogeko/movisea/pull/41) [`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774) Thanks [@mogeko](https://github.com/mogeko)! - Upgrade dependencies
8 |
9 | - bump `@types/node` from `20.2.5` to `20.3.3` ([#37](https://github.com/mogeko/movisea/pull/37))
10 |
11 | ## 0.0.2
12 |
13 | ### Patch Changes
14 |
15 | - [#28](https://github.com/mogeko/movisea/pull/28) [`f02efa6`](https://github.com/mogeko/movisea/commit/f02efa69403ef02284b49ff0e0e7b050a9b4c99c) Thanks [@mogeko](https://github.com/mogeko)! - Rename to `@mogeko/is-plain-object`
16 |
17 | ## 0.0.1
18 |
19 | ### Patch Changes
20 |
21 | - [#26](https://github.com/mogeko/movisea/pull/26) [`dcb257f`](https://github.com/mogeko/movisea/commit/dcb257fb5deec590631f3874c1e319d15b8345e1) Thanks [@mogeko](https://github.com/mogeko)! - Implement our own `is-plain-object`
22 |
--------------------------------------------------------------------------------
/apps/stories/.storybook/preview.tsx:
--------------------------------------------------------------------------------
1 | import "../../web/styles/globals.css";
2 | import "../.storybook/stubs/next-image";
3 |
4 | import * as React from "react";
5 | import { withThemeByClassName } from "@storybook/addon-styling";
6 | import type { Decorator, Parameters } from "@storybook/react";
7 |
8 | import { Toaster } from "@/components/toaster";
9 |
10 | export const parameters: Parameters = {
11 | actions: { argTypesRegex: "^on[A-Z].*" },
12 | controls: {
13 | matchers: {
14 | color: /(background|color)$/i,
15 | date: /Date$/,
16 | },
17 | },
18 | backgrounds: { disable: true },
19 | nextjs: { appDirectory: true },
20 | };
21 |
22 | export const decorators: Array = [
23 | (Story) => (
24 |
25 |
26 |
27 |
28 | ),
29 | withThemeByClassName({
30 | themes: {
31 | light: "light",
32 | dark: "dark",
33 | },
34 | defaultTheme: "light",
35 | }),
36 | ];
37 |
--------------------------------------------------------------------------------
/apps/web/components/poster-image.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image, { type ImageLoader } from "next/image";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | export const PosterImage: React.FC<
8 | Omit, "loader">
9 | > = ({ width = 300, loading = "lazy", alt, className, ...props }) => {
10 | return (
11 |
23 | );
24 | };
25 |
26 | const posterLoader: ImageLoader = ({ src, width }) => {
27 | if (width > 1000) {
28 | return `https://image.tmdb.org/t/p/w1280${src}`;
29 | } else {
30 | const height = width * 1.5;
31 | return `https://image.tmdb.org/t/p/w${width}_and_h${height}_bestv2${src}`;
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/apps/stories/shadcn-ui/separator.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { Separator } from "@/components/ui/separator";
5 |
6 | export default {
7 | title: "Shadcn-ui/Separator",
8 | component: Separator,
9 | } as Meta;
10 | type Story = StoryObj;
11 |
12 | export const Default: Story = {};
13 |
14 | export const Demo1: Story = {
15 | render: ({ className, ...props }) => (
16 |
17 |
18 |
Radix Primitives
19 |
20 | An open-source UI component library.
21 |
22 |
23 |
24 |
25 |
Blog
26 |
27 |
Docs
28 |
29 |
Source
30 |
31 |
32 | ),
33 | };
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Zheng Junyi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/stories/shadcn-ui/badge.stories.tsx:
--------------------------------------------------------------------------------
1 | import { default as NextLink } from "next/link";
2 | import type { Meta, StoryObj } from "@storybook/react";
3 |
4 | import { Badge, badgeVariants } from "@/components/ui/badge";
5 |
6 | export default {
7 | title: "Shadcn-ui/Badge",
8 | component: Badge,
9 | } as Meta;
10 | type Story = StoryObj;
11 |
12 | export const Default: Story = {
13 | args: {
14 | children: "Badge",
15 | },
16 | };
17 |
18 | export const Secondary: Story = {
19 | args: {
20 | children: "Badge",
21 | variant: "secondary",
22 | },
23 | };
24 |
25 | export const Destructive: Story = {
26 | args: {
27 | children: "Badge",
28 | variant: "destructive",
29 | },
30 | };
31 |
32 | export const Outline: Story = {
33 | args: {
34 | children: "Badge",
35 | variant: "outline",
36 | },
37 | };
38 |
39 | export const Link: Story = {
40 | render: (args) => (
41 |
42 | {args.children}
43 |
44 | ),
45 | args: {
46 | children: "Link",
47 | variant: "outline",
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/packages/tmdb-api/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Zheng Junyi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/tmdb-request/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Zheng Junyi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/is-plain-object/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Zheng Junyi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/tmdb-api/src/mod.ts:
--------------------------------------------------------------------------------
1 | import { ENDPOINTS } from "@/endpoints";
2 | import { parser, request, type Options } from "@mogeko/tmdb-request";
3 | import { mergeDeep } from "@mogeko/tmdb-request/merge-deep";
4 |
5 | import type { Recur, RestInterface } from "@/types/mod";
6 |
7 | export class TMDB {
8 | private _defaultOpts: Options;
9 |
10 | constructor({ auth }: { auth: `Bearer ${string}` }) {
11 | this._defaultOpts = { headers: { authorization: auth } };
12 | }
13 |
14 | public rest: Recur = Object.fromEntries(
15 | Object.entries(ENDPOINTS).map(([topLevelKey, subs]) => [
16 | topLevelKey,
17 | Object.fromEntries(
18 | Object.entries(subs).map(([subLevelKey, [route, opts]]) => [
19 | subLevelKey,
20 | (params: any) => this.request(route, mergeDeep(opts, params)),
21 | ])
22 | ),
23 | ])
24 | ) as any;
25 |
26 | public request(route: string, opts: Options = {}) {
27 | return request(route, mergeDeep(this._defaultOpts, opts));
28 | }
29 |
30 | public parser(route: string, opts: Options = {}) {
31 | return parser(route, mergeDeep(this._defaultOpts, opts));
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/stories/app/search/components/pagination.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 |
3 | import { Pagination } from "@/app/search/components/pagination";
4 |
5 | export default {
6 | title: "APP/Search/Components/Pagination",
7 | component: Pagination,
8 | } as Meta;
9 | type Story = StoryObj;
10 |
11 | export const Default: Story = {
12 | args: {
13 | totalPages: 5,
14 | },
15 | };
16 |
17 | export const InPage3: Story = {
18 | args: {
19 | totalPages: 9,
20 | },
21 | parameters: {
22 | nextjs: {
23 | navigation: { query: { page: "3" } },
24 | },
25 | },
26 | };
27 |
28 | export const MoreThan9Pages: Story = {
29 | args: {
30 | totalPages: 10,
31 | },
32 | };
33 |
34 | export const InPage10With20Pages: Story = {
35 | args: {
36 | totalPages: 20,
37 | },
38 | parameters: {
39 | nextjs: {
40 | navigation: { query: { page: "10" } },
41 | },
42 | },
43 | };
44 |
45 | export const InPage20With20Pages: Story = {
46 | args: {
47 | totalPages: 20,
48 | },
49 | parameters: {
50 | nextjs: {
51 | navigation: { query: { page: "20" } },
52 | },
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/apps/stories/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movisea-stories",
3 | "version": "0.0.13",
4 | "private": true,
5 | "scripts": {
6 | "dev": "storybook dev -p 6006",
7 | "build": "storybook build -o dist",
8 | "type-check": "tsc --noEmit",
9 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
10 | "fmt": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
11 | },
12 | "dependencies": {
13 | "movisea-web": "workspace:*",
14 | "react": "18.2.0",
15 | "react-dom": "18.2.0"
16 | },
17 | "devDependencies": {
18 | "@storybook/addon-essentials": "^7.0.26",
19 | "@storybook/addon-interactions": "^7.0.24",
20 | "@storybook/addon-links": "^7.0.26",
21 | "@storybook/addon-styling": "^1.3.4",
22 | "@storybook/blocks": "^7.0.24",
23 | "@storybook/nextjs": "^7.0.24",
24 | "@storybook/react": "^7.0.27",
25 | "@storybook/testing-library": "^0.2.0",
26 | "@types/node": "20.4.2",
27 | "@types/react": "18.2.8",
28 | "@types/react-dom": "18.2.4",
29 | "autoprefixer": "10.4.14",
30 | "postcss": "8.4.24",
31 | "storybook": "^7.0.27",
32 | "tailwindcss": "3.3.2",
33 | "tailwindcss-animate": "^1.0.6",
34 | "typescript": "5.1.6"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 |
7 | concurrency: ${{ github.workflow }}-${{ github.ref }}
8 |
9 | permissions:
10 | contents: write
11 | pull-requests: write
12 |
13 | jobs:
14 | relsease:
15 | name: Create PR or Publish to NPM
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Set up pnpm
20 | uses: pnpm/action-setup@v2.2.4
21 | with:
22 | version: 8
23 | - name: Set up Node.js
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: 20
27 | cache: "pnpm"
28 | - run: pnpm install -rw
29 | - run: pnpm run build
30 | - name: Create Version PR or Publish to NPM
31 | id: changesets
32 | uses: changesets/action@v1.4.5
33 | with:
34 | commit: "chore(release): version packages"
35 | title: "chore(release): version packages"
36 | version: node .github/changeset-version.cjs
37 | publish: pnpm changeset publish
38 | env:
39 | NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | NODE_ENV: "production"
42 |
--------------------------------------------------------------------------------
/apps/web/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
13 | secondary:
14 | "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
15 | destructive:
16 | "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/apps/web/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SwitchPrimitives from "@radix-ui/react-switch";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/apps/web/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css";
2 |
3 | import type { Metadata } from "next";
4 |
5 | import { siteConfig } from "@/config/site";
6 | import { sans } from "@/lib/fonts";
7 | import { cn } from "@/lib/utils";
8 | import { SiteFooter } from "@/components/site-footer";
9 | import { SiteHeader } from "@/components/site-header";
10 | import { ThemeProvider } from "@/components/theme-provider";
11 | import { Toaster } from "@/components/toaster";
12 |
13 | export const metadata: Metadata = {
14 | title: siteConfig.name,
15 | description: siteConfig.description,
16 | };
17 |
18 | const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
19 | return (
20 |
21 |
22 |
28 |
29 |
30 |
31 |
{children}
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default RootLayout;
42 |
--------------------------------------------------------------------------------
/apps/web/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/is-plain-object/README.md:
--------------------------------------------------------------------------------
1 | # is-plain-object
2 |
3 | > Returns true if an object was created by the `Object` constructor, or Object.create(null).
4 |
5 | This project originated from [is-plain-object](https://github.com/jonschlinkert/is-plain-object) (released under [MIT license](https://github.com/jonschlinkert/is-plain-object/blob/master/LICENSE)). I reimplemented it by TypeScript.
6 |
7 | ## Usage
8 |
9 | With ES modules:
10 |
11 | ```ts
12 | import { isPlainObject } from "is-plain-object";
13 | ```
14 |
15 | **true** when created by the `Object` constructor, or `Object.create(null)`.
16 |
17 | ```ts
18 | isPlainObject(Object.create({})); // => true
19 | isPlainObject(Object.create(Object.prototype)); // => true
20 | isPlainObject({ foo: "bar" }); // => true
21 | isPlainObject({}); // => true
22 | isPlainObject(Object.create(null)); // => true
23 | ```
24 |
25 | **false** when not created by the `Object` constructor.
26 |
27 | ```ts
28 | isPlainObject(["foo", "bar"]); // => false
29 | isPlainObject([]); // => false
30 | isPlainObject(new Foo()); // => false
31 | ```
32 |
33 | ## License
34 |
35 | The [original project](https://github.com/jonschlinkert/is-plain-object) is released under the [MIT License](https://github.com/jonschlinkert/is-plain-object/blob/master/LICENSE).
36 |
37 | The code in this project is released under the [MIT License](./LICENSE).
38 |
--------------------------------------------------------------------------------
/apps/web/lib/use-localstorage.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback, useEffect, useState } from "react";
4 | import type { Dispatch, SetStateAction } from "react";
5 |
6 | export function useLocalStorage(storageKey: string, init = "") {
7 | const [value, setValue] = useState(init);
8 |
9 | // In order to ensure that `window.*` code runs only in the client
10 | // See: https://github.com/vercel/next.js/discussions/19911
11 | useEffect(() => {
12 | setValue(window.localStorage.getItem(storageKey) || init);
13 | }, [storageKey, init]);
14 |
15 | const setItem: Dispatch> = (newValue) => {
16 | if (typeof newValue === "function") {
17 | return setItem(newValue(value));
18 | } else {
19 | setValue(newValue);
20 |
21 | if (newValue === init) {
22 | window.localStorage.removeItem(storageKey);
23 | } else {
24 | window.localStorage.setItem(storageKey, newValue);
25 | }
26 | }
27 | };
28 |
29 | const handleStorage = useCallback(
30 | (e: StorageEvent) => {
31 | if (e.key !== storageKey) return;
32 | if (e.newValue !== value) {
33 | setValue(e.newValue || init);
34 | }
35 | },
36 | [value, init, storageKey]
37 | );
38 |
39 | useEffect(() => {
40 | window.addEventListener("storage", handleStorage);
41 | return () => window.removeEventListener("storage", handleStorage);
42 | }, [handleStorage]);
43 |
44 | return [value, setItem] as const;
45 | }
46 |
--------------------------------------------------------------------------------
/packages/tmdb-api/src/types/movie/details.ts:
--------------------------------------------------------------------------------
1 | import { Coutry, ISO_639_1, ISO_3166_1, Language } from "@/types/shared";
2 |
3 | export type MovieDifferent = {
4 | media_type: "movie";
5 | title: string;
6 | original_title: string;
7 | release_date: string;
8 | };
9 |
10 | export type MovieDetailsResult = {
11 | belongs_to_collection: null;
12 | budget: number;
13 | genres: Array<{
14 | id: number;
15 | name: string;
16 | }>;
17 | homepage: string;
18 | imdb_id: `tt${number}`;
19 | production_companies: Array<{
20 | id: number;
21 | logo_path: string;
22 | name: string;
23 | origin_country: Coutry;
24 | }>;
25 | production_countries: Array<{
26 | iso_3166_1: ISO_3166_1;
27 | name: string;
28 | }>;
29 | revenue: number;
30 | runtime: number;
31 | spoken_languages: Array<{
32 | english_name: string;
33 | iso_639_1: ISO_639_1;
34 | name: string;
35 | }>;
36 | status:
37 | | "Rumored"
38 | | "Planned"
39 | | "In Production"
40 | | "Post Production"
41 | | "Released"
42 | | "Canceled";
43 | tagline: string;
44 | video: boolean;
45 | } & Omit &
46 | MovieAndTVShared;
47 |
48 | export type MovieAndTVShared = {
49 | ault: boolean;
50 | backdrop_path: string;
51 | id: number;
52 | original_language: Language;
53 | overview: string;
54 | poster_path: string;
55 | genre_ids: number[];
56 | popularity: number;
57 | vote_average: number;
58 | vote_count: number;
59 | };
60 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | ```
14 |
15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16 |
17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
18 |
19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movisea-web",
3 | "version": "0.0.13",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "type-check": "tsc --noEmit",
10 | "lint": "next lint",
11 | "fmt": "next lint --fix"
12 | },
13 | "dependencies": {
14 | "@hookform/resolvers": "^3.1.1",
15 | "@mogeko/tmdb-request": "workspace:^",
16 | "@radix-ui/react-accordion": "^1.1.2",
17 | "@radix-ui/react-avatar": "^1.0.3",
18 | "@radix-ui/react-dialog": "^1.0.4",
19 | "@radix-ui/react-dropdown-menu": "^2.0.5",
20 | "@radix-ui/react-label": "^2.0.2",
21 | "@radix-ui/react-separator": "^1.0.3",
22 | "@radix-ui/react-slot": "^1.0.2",
23 | "@radix-ui/react-switch": "^1.0.3",
24 | "@radix-ui/react-toast": "^1.1.4",
25 | "class-variance-authority": "^0.6.1",
26 | "clsx": "^2.0.0",
27 | "cmdk": "^0.2.0",
28 | "next": "13.4.9",
29 | "next-themes": "^0.2.1",
30 | "react": "18.2.0",
31 | "react-dom": "18.2.0",
32 | "react-hook-form": "^7.45.1",
33 | "react-icons": "^4.10.1",
34 | "tailwind-merge": "^1.14.0",
35 | "zod": "^3.21.4"
36 | },
37 | "devDependencies": {
38 | "@tsconfig/next": "^2.0.0",
39 | "@types/node": "20.4.2",
40 | "@types/react": "18.2.8",
41 | "@types/react-dom": "18.2.4",
42 | "autoprefixer": "10.4.14",
43 | "postcss": "8.4.24",
44 | "tailwindcss": "3.3.2",
45 | "tailwindcss-animate": "^1.0.6",
46 | "typescript": "5.1.6"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movisea",
3 | "version": "0.0.1",
4 | "private": true,
5 | "description": "Front-end implementation of The Movie Database (TMDB)",
6 | "author": {
7 | "name": "Zheng Junyi",
8 | "email": "zhengjunyi@live.com"
9 | },
10 | "repository": "github:mogeko/movisea",
11 | "homepage": "https://github.com/mogeko/movisea#readme",
12 | "bugs": {
13 | "url": "https://github.com/mogeko/movisea/issues",
14 | "email": "zhengjunyi@live.com"
15 | },
16 | "license": "MIT",
17 | "scripts": {
18 | "dev": "turbo run --filter=movisea-web dev",
19 | "storybook": "turbo run --filter=movisea-stories dev",
20 | "build": "turbo run build",
21 | "start": "turbo run start",
22 | "test": "turbo run test",
23 | "cov": "turbo run cov",
24 | "type-check": "turbo run type-check",
25 | "lint": "turbo run lint",
26 | "fmt": "turbo run fmt"
27 | },
28 | "workspaces": [
29 | "apps/*",
30 | "packages/*"
31 | ],
32 | "devDependencies": {
33 | "@changesets/changelog-github": "^0.4.8",
34 | "@changesets/cli": "^2.26.2",
35 | "@ianvs/prettier-plugin-sort-imports": "^4.1.0",
36 | "eslint": "8.45.0",
37 | "eslint-config-next": "13.4.4",
38 | "eslint-config-prettier": "^8.8.0",
39 | "eslint-config-turbo": "^1.10.8",
40 | "eslint-plugin-prettier": "^4.2.1",
41 | "eslint-plugin-storybook": "^0.6.12",
42 | "prettier": "^2.8.8",
43 | "prettier-plugin-tailwindcss": "^0.3.0",
44 | "turbo": "^1.10.8",
45 | "typescript": "^5.1.6"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/apps/stories/shadcn-ui/button.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 | import { LuLoader2, LuMail } from "react-icons/lu";
3 |
4 | import { Button } from "@/components/ui/button";
5 |
6 | export default {
7 | title: "Shadcn-ui/Button",
8 | component: Button,
9 | } as Meta;
10 | type Story = StoryObj;
11 |
12 | export const Primary: Story = {
13 | args: {
14 | children: "Primary",
15 | },
16 | };
17 |
18 | export const Secondary: Story = {
19 | args: {
20 | children: "Secondary",
21 | variant: "secondary",
22 | },
23 | };
24 |
25 | export const Destructive: Story = {
26 | args: {
27 | children: "Destructive",
28 | variant: "destructive",
29 | },
30 | };
31 |
32 | export const Outline: Story = {
33 | args: {
34 | children: "Outline",
35 | variant: "outline",
36 | },
37 | };
38 |
39 | export const Ghost: Story = {
40 | args: {
41 | children: "Ghost",
42 | variant: "ghost",
43 | },
44 | };
45 |
46 | export const Link: Story = {
47 | args: {
48 | children: "Link",
49 | variant: "link",
50 | },
51 | };
52 |
53 | export const WithIcon: Story = {
54 | render: (args) => (
55 |
58 | ),
59 | };
60 |
61 | export const Loading: Story = {
62 | render: (args) => (
63 |
67 | ),
68 | args: {
69 | disabled: true,
70 | },
71 | };
72 |
--------------------------------------------------------------------------------
/apps/web/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { LuLaptop, LuMoon, LuSunMedium } from "react-icons/lu";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu";
13 |
14 | export const ModeToggle: React.FC = () => {
15 | const { setTheme } = useTheme();
16 |
17 | return (
18 |
19 |
20 |
25 |
26 |
27 | setTheme("light")}>
28 |
29 | Light
30 |
31 | setTheme("dark")}>
32 |
33 | Dark
34 |
35 | setTheme("system")}>
36 |
37 | System
38 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/packages/tmdb-request/test/parser.test.ts:
--------------------------------------------------------------------------------
1 | import { parser } from "@/mod.ts";
2 | import { describe, expect, it } from "vitest";
3 |
4 | describe("parser", () => {
5 | it("should parse route", () => {
6 | const context = parser("/foo/{bar}", {
7 | headers: { authorization: "Bearer xxx" },
8 | bar: "baz",
9 | });
10 |
11 | expect(context.method).toEqual("GET");
12 | expect(context.url).toEqual("/foo/baz");
13 | expect(context.headers.authorization).toEqual("Bearer xxx");
14 |
15 | delete context.headers["user-agent"];
16 | expect(context).toMatchSnapshot();
17 | });
18 |
19 | it("should parse endpoint", () => {
20 | const context = parser({
21 | url: "POST /foo/{bar}",
22 | headers: {
23 | authorization: "Bearer xxx",
24 | "content-type": "application/json",
25 | },
26 | body: JSON.stringify({ foo: "bar" }),
27 | bar: "baz",
28 | });
29 |
30 | expect(context.method).toEqual("POST");
31 | expect(context.url).toEqual("/foo/baz");
32 | expect(context.headers.authorization).toEqual("Bearer xxx");
33 | expect(context.headers["content-type"]).toEqual("application/json");
34 |
35 | delete context.headers["user-agent"];
36 | expect(context).toMatchSnapshot();
37 | });
38 |
39 | it("should be covered by route", () => {
40 | expect(
41 | parser("/foo/bar", { url: "/This/should/be/covered/by/route" }).url
42 | ).toEqual("/foo/bar");
43 | });
44 |
45 | it("should pass undefined values", () => {
46 | expect(parser("/foo/{bar}", { baz: "bar" }).url).toEqual("/foo/");
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/packages/is-plain-object/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mogeko/is-plain-object",
3 | "version": "0.0.3",
4 | "private": false,
5 | "type": "module",
6 | "description": "Returns true if an object was created by the `Object` constructor, or Object.create(null)",
7 | "author": {
8 | "name": "Zheng Junyi",
9 | "email": "zhengjunyi@live.com"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/mogeko/movisea.git",
14 | "directory": "packages/tmdb-api"
15 | },
16 | "homepage": "https://github.com/mogeko/movisea/tree/master/packages/tmdb-api#readme",
17 | "bugs": {
18 | "url": "https://github.com/mogeko/movisea/issues",
19 | "email": "zhengjunyi@live.com"
20 | },
21 | "keywords": [
22 | "type",
23 | "check",
24 | "is",
25 | "plain",
26 | "object"
27 | ],
28 | "license": "MIT",
29 | "main": "dist/is-plain-object.js",
30 | "types": "dist/is-plain-object.d.ts",
31 | "files": [
32 | "!dist/metafile-*.json",
33 | "!dist/*.map",
34 | "dist",
35 | "CHANGELOG.md"
36 | ],
37 | "scripts": {
38 | "build": "tsup",
39 | "test": "vitest run",
40 | "cov": "vitest run --coverage",
41 | "type-check": "tsc --noEmit",
42 | "lint": "prettier --check \"src/**/*.{js,ts}\" --cache",
43 | "fmt": "prettier --write \"src/**/*.{js,ts}\" --cache"
44 | },
45 | "devDependencies": {
46 | "@types/node": "20.4.2",
47 | "@vitest/coverage-v8": "^0.33.0",
48 | "tsconfig": "workspace:*",
49 | "tsup": "^7.1.0",
50 | "typescript": "^5.1.6",
51 | "vite-tsconfig-paths": "^4.2.0",
52 | "vitest": "^0.33.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/tmdb-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mogeko/tmdb-api",
3 | "version": "0.1.8",
4 | "private": false,
5 | "type": "module",
6 | "description": "A HTTP client for TMDB APIs",
7 | "author": {
8 | "name": "Zheng Junyi",
9 | "email": "zhengjunyi@live.com"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/mogeko/movisea.git",
14 | "directory": "packages/tmdb-api"
15 | },
16 | "homepage": "https://github.com/mogeko/movisea/tree/master/packages/tmdb-api#readme",
17 | "bugs": {
18 | "url": "https://github.com/mogeko/movisea/issues",
19 | "email": "zhengjunyi@live.com"
20 | },
21 | "keywords": [
22 | "themoviedb",
23 | "tmdb",
24 | "api"
25 | ],
26 | "license": "MIT",
27 | "main": "dist/tmdb-api.js",
28 | "types": "dist/tmdb-api.d.ts",
29 | "files": [
30 | "!dist/metafile-*.json",
31 | "!dist/*.map",
32 | "dist",
33 | "CHANGELOG.md"
34 | ],
35 | "scripts": {
36 | "build": "tsup",
37 | "test": "vitest run",
38 | "cov": "vitest run --coverage",
39 | "type-check": "tsc --noEmit",
40 | "lint": "prettier --check \"src/**/*.{js,ts}\" --cache",
41 | "fmt": "prettier --write \"src/**/*.{js,ts}\" --cache",
42 | "release": "pnpm publish --access public"
43 | },
44 | "dependencies": {
45 | "@mogeko/tmdb-request": "workspace:^"
46 | },
47 | "devDependencies": {
48 | "@types/node": "20.4.2",
49 | "@vitest/coverage-v8": "^0.33.0",
50 | "tsconfig": "workspace:*",
51 | "tsup": "^7.1.0",
52 | "typescript": "^5.1.6",
53 | "vite-tsconfig-paths": "^4.2.0",
54 | "vitest": "^0.33.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/apps/web/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/apps/web/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 |
10 | --muted: 240 4.8% 95.9%;
11 | --muted-foreground: 240 3.8% 46.1%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 240 10% 3.9%;
18 |
19 | --border: 240 5.9% 90%;
20 | --input: 240 5.9% 90%;
21 |
22 | --primary: 240 5.9% 10%;
23 | --primary-foreground: 0 0% 98%;
24 |
25 | --secondary: 240 4.8% 95.9%;
26 | --secondary-foreground: 240 5.9% 10%;
27 |
28 | --accent: 240 4.8% 95.9%;
29 | --accent-foreground: ;
30 |
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 0 0% 98%;
33 |
34 | --ring: 240 5% 64.9%;
35 |
36 | --radius: 0.5rem;
37 | }
38 |
39 | .dark {
40 | --background: 240 10% 3.9%;
41 | --foreground: 0 0% 98%;
42 |
43 | --muted: 240 3.7% 15.9%;
44 | --muted-foreground: 240 5% 64.9%;
45 |
46 | --popover: 240 10% 3.9%;
47 | --popover-foreground: 0 0% 98%;
48 |
49 | --card: 240 10% 3.9%;
50 | --card-foreground: 0 0% 98%;
51 |
52 | --border: 240 3.7% 15.9%;
53 | --input: 240 3.7% 15.9%;
54 |
55 | --primary: 0 0% 98%;
56 | --primary-foreground: 240 5.9% 10%;
57 |
58 | --secondary: 240 3.7% 15.9%;
59 | --secondary-foreground: 0 0% 98%;
60 |
61 | --accent: 240 3.7% 15.9%;
62 | --accent-foreground: ;
63 |
64 | --destructive: 0 62.8% 30.6%;
65 | --destructive-foreground: 0 85.7% 97.3%;
66 |
67 | --ring: 240 3.7% 15.9%;
68 | }
69 | }
70 |
71 | @layer base {
72 | * {
73 | @apply border-border;
74 | }
75 | body {
76 | @apply bg-background text-foreground;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/tmdb-api/test/tmdb.test.ts:
--------------------------------------------------------------------------------
1 | import { TMDB } from "@/mod";
2 | import { beforeEach, describe, expect, it, vi } from "vitest";
3 |
4 | const tmdb = new TMDB({ auth: "Bearer ONLY_FOR_TESTING" });
5 |
6 | describe("Re-exported form tmdb-request", () => {
7 | beforeEach(() => (vi.resetAllMocks(), void 0));
8 |
9 | it("constructor", () => {
10 | expect(tmdb).toBeDefined();
11 | });
12 |
13 | it("should request", async () => {
14 | const spy = vi.spyOn(tmdb, "request").mockImplementation(async () => {
15 | return { test: "test" };
16 | });
17 |
18 | expect(
19 | await tmdb.request("/movie/{id}?language={language}", {
20 | language: "en-US",
21 | id: 10997,
22 | })
23 | ).toEqual({ test: "test" });
24 |
25 | expect(spy).toHaveBeenCalledOnce();
26 | });
27 |
28 | it("should parse", () => {
29 | const context = tmdb.parser("/foo/{bar}", { bar: "baz" });
30 |
31 | expect(context.method).toEqual("GET");
32 | expect(context.url).toEqual("/foo/baz");
33 | expect(context.headers?.authorization).toEqual("Bearer ONLY_FOR_TESTING");
34 |
35 | delete context.headers?.["user-agent"];
36 | expect(context).toMatchSnapshot();
37 | });
38 | });
39 |
40 | describe("TMDB REST API", () => {
41 | beforeEach(() => (vi.resetAllMocks(), void 0));
42 |
43 | it("should get movie details", async () => {
44 | const spy = vi.spyOn(tmdb, "request").mockImplementation(async () => {
45 | return { test: "test" };
46 | });
47 |
48 | const result = await tmdb.rest.movie.details({ id: 10997 });
49 |
50 | expect(spy).toHaveBeenCalledOnce();
51 | expect(spy).toHaveBeenCalledWith(
52 | "GET /movie/{id}?append_to_response={append_to_response}&language={language}",
53 | { language: "en-US", id: 10997 }
54 | );
55 | expect(result).toEqual({ test: "test" });
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/packages/tmdb-request/src/merge-deep.ts:
--------------------------------------------------------------------------------
1 | import { isPlainObject } from "@mogeko/is-plain-object";
2 |
3 | /**
4 | * Deep merge two objects.
5 | *
6 | * It will merge any plain object properties recursively. If the property
7 | * is not a plain object, it will be covered by the right hand object.
8 | *
9 | * @param lObj The left hand object to be merged
10 | * @param rObj The right hand object to be merged
11 | * @returns The merged object
12 | *
13 | * @example
14 | * ```ts
15 | * mergeDeep({ foo: { bar1: "baz" } }, { foo: { bar2: "qux" } });
16 | * // => { foo: { bar1: "baz", bar2: "qux" } }
17 | *
18 | * mergeDeep({ foo: { bar: "baz" } }, { foo: { bar: "qux" } });
19 | * // => { foo: { bar: "qux" } }
20 | * ```
21 | */
22 | export function mergeDeep<
23 | L extends Record,
24 | R extends Record
25 | >(lObj: L, rObj: R): L & R {
26 | const result: Record = Object.assign({}, lObj);
27 |
28 | Object.keys(rObj).forEach((key) => {
29 | if (isPlainObject(rObj[key])) {
30 | if (!(key in lObj)) {
31 | Object.assign(result, { [key]: rObj[key] });
32 | } else {
33 | result[key] = mergeDeep(lObj[key], rObj[key]);
34 | }
35 | } else {
36 | Object.assign(result, { [key]: rObj[key] });
37 | }
38 | });
39 |
40 | return result;
41 | }
42 |
43 | if (import.meta.vitest) {
44 | const { describe, it, expect } = await import("vitest");
45 |
46 | describe("mergeDeep", () => {
47 | it("should merge objects", () => {
48 | expect(
49 | mergeDeep({ foo: { bar1: "baz" } }, { foo: { bar2: "qux" } })
50 | ).toEqual({ foo: { bar1: "baz", bar2: "qux" } });
51 | });
52 |
53 | it("should cover by right hand", () => {
54 | expect(
55 | mergeDeep({ foo: { bar: "baz" } }, { foo: { bar: "qux" } })
56 | ).toEqual({ foo: { bar: "qux" } });
57 | });
58 | });
59 | }
60 |
--------------------------------------------------------------------------------
/packages/tmdb-request/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mogeko/tmdb-request",
3 | "version": "1.3.2",
4 | "private": false,
5 | "type": "module",
6 | "description": "A simple wrapper for TMDB APIs",
7 | "author": {
8 | "name": "Zheng Junyi",
9 | "email": "zhengjunyi@live.com"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/mogeko/movisea.git",
14 | "directory": "packages/tmdb-request"
15 | },
16 | "homepage": "https://github.com/mogeko/movisea/tree/master/packages/tmdb-request#readme",
17 | "bugs": {
18 | "url": "https://github.com/mogeko/movisea/issues",
19 | "email": "zhengjunyi@live.com"
20 | },
21 | "keywords": [
22 | "themoviedb",
23 | "tmdb",
24 | "api"
25 | ],
26 | "license": "MIT",
27 | "main": "dist/tmdb-request.js",
28 | "types": "dist/tmdb-request.d.ts",
29 | "exports": {
30 | ".": {
31 | "import": "./dist/tmdb-request.js",
32 | "types": "./dist/tmdb-request.d.ts"
33 | },
34 | "./merge-deep": {
35 | "import": "./dist/merge-deep.js",
36 | "types": "./dist/merge-deep.d.ts"
37 | }
38 | },
39 | "files": [
40 | "!dist/metafile-*.json",
41 | "!dist/*.map",
42 | "dist",
43 | "CHANGELOG.md"
44 | ],
45 | "scripts": {
46 | "build": "tsup",
47 | "test": "vitest run",
48 | "cov": "vitest run --coverage",
49 | "type-check": "tsc --noEmit",
50 | "lint": "prettier --check \"src/**/*.{js,ts}\" --cache",
51 | "fmt": "prettier --write \"src/**/*.{js,ts}\" --cache",
52 | "release": "pnpm publish --access public"
53 | },
54 | "dependencies": {
55 | "@mogeko/is-plain-object": "workspace:^",
56 | "universal-user-agent": "^7.0.1",
57 | "url-template": "^3.1.0"
58 | },
59 | "devDependencies": {
60 | "@types/node": "20.4.2",
61 | "@vitest/coverage-v8": "^0.33.0",
62 | "tsconfig": "workspace:*",
63 | "tsup": "^7.1.0",
64 | "typescript": "^5.1.6",
65 | "vite-tsconfig-paths": "^4.2.0",
66 | "vitest": "^0.33.0"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/apps/web/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | }
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/apps/stories/shadcn-ui/accordion.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 |
3 | import {
4 | Accordion,
5 | AccordionContent,
6 | AccordionItem,
7 | AccordionTrigger,
8 | } from "@/components/ui/accordion";
9 |
10 | export default {
11 | title: "Shadcn-ui/Accordion",
12 | component: Accordion,
13 | argTypes: {
14 | type: {
15 | options: ["single", "multiple"],
16 | control: { type: "select" },
17 | },
18 | },
19 | } as Meta;
20 | type Story = StoryObj;
21 |
22 | export const Default: Story = {
23 | render: (args) => (
24 |
25 |
26 | Is it accessible?
27 |
28 | Yes. It adheres to the WAI-ARIA design pattern.
29 |
30 |
31 |
32 | ),
33 | args: {
34 | collapsible: true,
35 | type: "single",
36 | },
37 | };
38 |
39 | export const Demo1: Story = {
40 | render: (args) => (
41 |
42 |
43 | Is it accessible?
44 |
45 | Yes. It adheres to the WAI-ARIA design pattern.
46 |
47 |
48 |
49 | Is it styled?
50 |
51 | Yes. It comes with default styles that matches the other
52 | components' aesthetic.
53 |
54 |
55 |
56 | Is it animated?
57 |
58 | Yes. It's animated by default, but you can disable it if you
59 | prefer.
60 |
61 |
62 |
63 | ),
64 | args: {
65 | className: "w-full",
66 | collapsible: true,
67 | type: "single",
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/apps/web/app/search/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { useParams, useSearchParams } from "next/navigation";
5 | import { MdLocalMovies, MdTv } from "react-icons/md";
6 | import { RiFireLine } from "react-icons/ri";
7 |
8 | import { cn } from "@/lib/utils";
9 | import { Button } from "@/components/ui/button";
10 |
11 | type SidebarProps = React.HTMLAttributes;
12 |
13 | export const Sidebar: React.FC = ({ className }) => {
14 | return (
15 |
16 |
17 |
18 |
19 | Filter by
20 |
21 |
22 |
23 |
24 | Top Results
25 |
26 |
27 |
28 | Movies
29 |
30 |
31 |
32 | TV Shows
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | const SidebarButton: React.FC<{
42 | mark: "multi" | "movie" | "tv";
43 | children?: React.ReactNode;
44 | }> = ({ mark, children }) => {
45 | const searchParams = useSearchParams();
46 | const { type } = useParams();
47 |
48 | if (type === mark) {
49 | return (
50 |
57 | );
58 | }
59 | return (
60 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/apps/web/app/movie/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { request } from "@mogeko/tmdb-request";
2 |
3 | import { tokens } from "@/config/tokens";
4 |
5 | const MoviePage: React.FC<{
6 | params: { id: string };
7 | }> = async ({ params }) => {
8 | const data = await getMovieInfo(params.id);
9 |
10 | return {/* TODO: Fill content */};
11 | };
12 |
13 | const getMovieInfo = async (id: string, params?: SearchParams) => {
14 | return request(
15 | "/movie/{id}?append_to_response={append_to_response}&language={language}",
16 | {
17 | headers: { authorization: `Bearer ${tokens.tmdb}` },
18 | append_to_response: params?.append_to_response ?? "",
19 | language: params?.language ?? "en-US",
20 | id: id,
21 | }
22 | ).catch((error) => {
23 | console.error(error);
24 | return null;
25 | });
26 | };
27 |
28 | type SearchParams = {
29 | append_to_response?: string;
30 | language?: string;
31 | };
32 |
33 | export type MovieInfo = {
34 | adult: boolean;
35 | backdrop_path: string;
36 | belongs_to_collection: null;
37 | budget: number;
38 | genres: Array<{
39 | id: number;
40 | name: string;
41 | }>;
42 | homepage: string;
43 | id: number;
44 | imdb_id: `tt${number}`;
45 | original_language: string;
46 | original_title: string;
47 | overview: string;
48 | popularity: number;
49 | poster_path: string;
50 | production_companies: Array<{
51 | id: number;
52 | logo_path: string;
53 | name: string;
54 | origin_country: string;
55 | }>;
56 | production_countries: Array<{
57 | iso_3166_1: string;
58 | name: string;
59 | }>;
60 | release_date: string;
61 | revenue: number;
62 | runtime: number;
63 | spoken_languages: Array<{
64 | english_name: string;
65 | iso_639_1: string;
66 | name: string;
67 | }>;
68 | status:
69 | | "Rumored"
70 | | "Planned"
71 | | "In Production"
72 | | "Post Production"
73 | | "Released"
74 | | "Canceled";
75 | tagline: string;
76 | title: string;
77 | video: boolean;
78 | vote_average: number;
79 | vote_count: number;
80 | };
81 |
82 | export default MoviePage;
83 |
--------------------------------------------------------------------------------
/apps/stories/shadcn-ui/form.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { zodResolver } from "@hookform/resolvers/zod";
3 | import type { Meta, StoryObj } from "@storybook/react";
4 | import { useForm } from "react-hook-form";
5 | import * as z from "zod";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | Form,
10 | FormControl,
11 | FormDescription,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form";
17 | import { Input } from "@/components/ui/input";
18 |
19 | export default {
20 | title: "Shadcn-ui/Form",
21 | component: Form,
22 | decorators: [
23 | (Story) => (
24 |
27 | ),
28 | ],
29 | } as Meta;
30 | type Story = StoryObj;
31 |
32 | const schema = z.object({
33 | username: z.string().min(2, {
34 | message: "Username must be at least 2 characters.",
35 | }),
36 | });
37 |
38 | const DemoForm: React.FC = () => {
39 | const form = useForm>({
40 | resolver: zodResolver(schema),
41 | defaultValues: {
42 | username: "",
43 | },
44 | });
45 | const onSubmit = (values: z.infer) => {
46 | console.log(values);
47 | };
48 |
49 | return (
50 |
51 |
71 |
72 |
73 | );
74 | };
75 |
76 | export const Demo1: Story = {
77 | render: (_args) => ,
78 | };
79 |
--------------------------------------------------------------------------------
/apps/web/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 | import { LuChevronDown } from "react-icons/lu";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
55 | {children}
56 |
57 | ));
58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
59 |
60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
61 |
--------------------------------------------------------------------------------
/apps/web/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = "CardTitle";
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = "CardDescription";
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = "CardContent";
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = "CardFooter";
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/packages/tmdb-request/test/request.test.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/mod.ts";
2 | import { getUserAgent } from "universal-user-agent";
3 | import { beforeEach, describe, expect, it, vi } from "vitest";
4 |
5 | describe("request", () => {
6 | beforeEach(() => (vi.resetAllMocks(), void 0));
7 |
8 | it("should request", async () => {
9 | const spy = vi.spyOn(global, "fetch").mockImplementation(async () => {
10 | return {
11 | json: vi.fn(async (_) => ({ test: "test" })),
12 | } as any;
13 | });
14 |
15 | const result = await request("/movie/{id}?language={language}", {
16 | headers: { authorization: "Bearer foo" },
17 | language: "en-US",
18 | id: 10997,
19 | });
20 |
21 | expect(spy).toHaveBeenCalledOnce();
22 | expect(spy).toHaveBeenCalledWith(
23 | "https://api.themoviedb.org/3/movie/10997?language=en-US",
24 | {
25 | method: "GET",
26 | headers: {
27 | accept: "application/json",
28 | authorization: "Bearer foo",
29 | "user-agent": getUserAgent(),
30 | },
31 | body: null,
32 | }
33 | );
34 | expect(result).toEqual({ test: "test" });
35 | });
36 |
37 | it("should request with endpoint", async () => {
38 | const spy = vi.spyOn(global, "fetch").mockImplementation(async () => {
39 | return {
40 | json: vi.fn(async (_) => ({ test: "test" })),
41 | } as any;
42 | });
43 |
44 | const result = await request({
45 | url: "POST /movie/{id}?language={language}",
46 | headers: {
47 | authorization: "Bearer foo",
48 | "content-type": "application/json",
49 | },
50 | body: JSON.stringify({ foo: "bar" }),
51 | language: "en-US",
52 | id: 10997,
53 | });
54 |
55 | expect(spy).toHaveBeenCalledOnce();
56 | expect(spy).toHaveBeenCalledWith(
57 | "https://api.themoviedb.org/3/movie/10997?language=en-US",
58 | {
59 | method: "POST",
60 | headers: {
61 | accept: "application/json",
62 | authorization: "Bearer foo",
63 | "content-type": "application/json",
64 | "user-agent": getUserAgent(),
65 | },
66 | body: '{"foo":"bar"}',
67 | }
68 | );
69 | expect(result).toEqual({ test: "test" });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: ["./{app,components}/**/*.{ts,tsx}"],
5 | theme: {
6 | container: {
7 | center: true,
8 | padding: "2rem",
9 | screens: {
10 | "2xl": "1400px",
11 | },
12 | },
13 | extend: {
14 | colors: {
15 | border: "hsl(var(--border))",
16 | input: "hsl(var(--input))",
17 | ring: "hsl(var(--ring))",
18 | background: "hsl(var(--background))",
19 | foreground: "hsl(var(--foreground))",
20 | primary: {
21 | DEFAULT: "hsl(var(--primary))",
22 | foreground: "hsl(var(--primary-foreground))",
23 | },
24 | secondary: {
25 | DEFAULT: "hsl(var(--secondary))",
26 | foreground: "hsl(var(--secondary-foreground))",
27 | },
28 | destructive: {
29 | DEFAULT: "hsl(var(--destructive))",
30 | foreground: "hsl(var(--destructive-foreground))",
31 | },
32 | muted: {
33 | DEFAULT: "hsl(var(--muted))",
34 | foreground: "hsl(var(--muted-foreground))",
35 | },
36 | accent: {
37 | DEFAULT: "hsl(var(--accent))",
38 | foreground: "hsl(var(--accent-foreground))",
39 | },
40 | popover: {
41 | DEFAULT: "hsl(var(--popover))",
42 | foreground: "hsl(var(--popover-foreground))",
43 | },
44 | card: {
45 | DEFAULT: "hsl(var(--card))",
46 | foreground: "hsl(var(--card-foreground))",
47 | },
48 | },
49 | borderRadius: {
50 | lg: "var(--radius)",
51 | md: "calc(var(--radius) - 2px)",
52 | sm: "calc(var(--radius) - 4px)",
53 | },
54 | keyframes: {
55 | "accordion-down": {
56 | from: { height: 0 },
57 | to: { height: "var(--radix-accordion-content-height)" },
58 | },
59 | "accordion-up": {
60 | from: { height: "var(--radix-accordion-content-height)" },
61 | to: { height: 0 },
62 | },
63 | },
64 | animation: {
65 | "accordion-down": "accordion-down 0.2s ease-out",
66 | "accordion-up": "accordion-up 0.2s ease-out",
67 | },
68 | },
69 | },
70 | plugins: [require("tailwindcss-animate")],
71 | };
72 |
--------------------------------------------------------------------------------
/packages/tmdb-api/src/types/account/mod.ts:
--------------------------------------------------------------------------------
1 | import type { AccountAddFavorite } from "@/types/account/add-favorite";
2 | import type { AccountAddWatchlist } from "@/types/account/add-watchlist";
3 | import type { AccountDetails } from "@/types/account/details";
4 | import type { AccountFavoriteMovies } from "@/types/account/favorite-movies";
5 | import type { AccountFavoriteTV } from "@/types/account/favorite-tv";
6 | import type { AccountGetParams } from "@/types/account/get-params";
7 | import type { AccountLists } from "@/types/account/lists";
8 | import type { AccountPostParams } from "@/types/account/post-params";
9 | import type { AccountRatedMovies } from "@/types/account/rated-movies";
10 | import type { AccountRatedTV } from "@/types/account/rated-tv";
11 | import type { AccountrRatedTVEpisodes } from "@/types/account/rated-tv-episodes";
12 | import type { AccountWatchlistMovies } from "@/types/account/watchlist-movies";
13 | import type { AccountWatchlistTV } from "@/types/account/watchlist-tv";
14 | import type { Assoc } from "@/types/utils";
15 |
16 | export type AccountInterface = Assoc<
17 | ["account", "detail"],
18 | (params: Pick) => AccountDetails
19 | > &
20 | Assoc<
21 | ["account", "addFavorite"],
22 | (params: AccountPostParams) => AccountAddFavorite
23 | > &
24 | Assoc<
25 | ["account", "addWatchlist"],
26 | (params: AccountPostParams) => AccountAddWatchlist
27 | > &
28 | Assoc<
29 | ["account", "favoriteMovies"],
30 | (params: AccountGetParams) => AccountFavoriteMovies
31 | > &
32 | Assoc<
33 | ["account", "favoriteTV"],
34 | (params: AccountGetParams) => AccountFavoriteTV
35 | > &
36 | Assoc<
37 | ["account", "lists"],
38 | (params: Omit) => AccountLists
39 | > &
40 | Assoc<
41 | ["account", "ratedMovies"],
42 | (params: AccountGetParams) => AccountRatedMovies
43 | > &
44 | Assoc<["account", "ratedTV"], (params: AccountGetParams) => AccountRatedTV> &
45 | Assoc<
46 | ["account", "ratedTVEpisodes"],
47 | (params: AccountGetParams) => AccountrRatedTVEpisodes
48 | > &
49 | Assoc<
50 | ["account", "watchlistMovies"],
51 | (params: AccountGetParams) => AccountWatchlistMovies
52 | > &
53 | Assoc<
54 | ["account", "watchlistTV"],
55 | (params: AccountGetParams) => AccountWatchlistTV
56 | >;
57 |
--------------------------------------------------------------------------------
/apps/stories/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # movisea-stories
2 |
3 | ## 0.0.13
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies []:
8 | - movisea-web@0.0.13
9 |
10 | ## 0.0.12
11 |
12 | ### Patch Changes
13 |
14 | - Updated dependencies []:
15 | - movisea-web@0.0.12
16 |
17 | ## 0.0.11
18 |
19 | ### Patch Changes
20 |
21 | - Updated dependencies []:
22 | - movisea-web@0.0.11
23 |
24 | ## 0.0.10
25 |
26 | ### Patch Changes
27 |
28 | - [#41](https://github.com/mogeko/movisea/pull/41) [`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774) Thanks [@mogeko](https://github.com/mogeko)! - Upgrade dependencies
29 |
30 | - bump `@storybook/testing-library` from `0.1.0` to `0.2.0` ([#40](https://github.com/mogeko/movisea/pull/40))
31 | - bump `@storybook/addon-styling` from `1.3.1` to `1.3.2` ([#39](https://github.com/mogeko/movisea/pull/39))
32 | - bump `eslint` from `8.41.0` to `8.44.0` ([#38](https://github.com/mogeko/movisea/pull/38))
33 | - bump `@types/node` from `20.2.5` to `20.3.3` ([#37](https://github.com/mogeko/movisea/pull/37))
34 | - bump `@tsconfig/next` from `1.0.5` to `2.0.0` ([#36](https://github.com/mogeko/movisea/pull/36))
35 |
36 | - Updated dependencies [[`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774)]:
37 | - movisea-web@0.0.10
38 |
39 | ## 0.0.9
40 |
41 | ### Patch Changes
42 |
43 | - Updated dependencies []:
44 | - movisea-web@0.0.9
45 |
46 | ## 0.0.8
47 |
48 | ### Patch Changes
49 |
50 | - Updated dependencies []:
51 | - movisea-web@0.0.8
52 |
53 | ## 0.0.7
54 |
55 | ### Patch Changes
56 |
57 | - Updated dependencies []:
58 | - movisea-web@0.0.7
59 |
60 | ## 0.0.6
61 |
62 | ### Patch Changes
63 |
64 | - Updated dependencies []:
65 | - movisea-web@0.0.6
66 |
67 | ## 0.0.5
68 |
69 | ### Patch Changes
70 |
71 | - Updated dependencies []:
72 | - movisea-web@0.0.5
73 |
74 | ## 0.0.4
75 |
76 | ### Patch Changes
77 |
78 | - Updated dependencies []:
79 | - movisea-web@0.0.4
80 |
81 | ## 0.0.3
82 |
83 | ### Patch Changes
84 |
85 | - [#9](https://github.com/mogeko/movisea/pull/9) [`294a53a`](https://github.com/mogeko/movisea/commit/294a53a67618da738f0e43510533a819de936385) Thanks [@mogeko](https://github.com/mogeko)! - Set the package to a private package
86 |
87 | - Updated dependencies []:
88 | - movisea-web@0.0.3
89 |
90 | ## 0.0.2
91 |
92 | ### Patch Changes
93 |
94 | - Updated dependencies []:
95 | - movisea-web@0.0.2
96 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
--------------------------------------------------------------------------------
/packages/is-plain-object/src/mod.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 Zheng Junyi. All rights reserved. Licensed under the MIT license.
2 |
3 | /**
4 | * This module provides a function to check if a value is a plain object.
5 | *
6 | * This project originated from jonschlinkert/is-plain-object (released under MIT license).
7 | *
8 | * @see {@link https://github.com/jonschlinkert/is-plain-object}
9 | *
10 | * @packageDocumentation
11 | */
12 |
13 | /**
14 | * Returns true if the given value is an object.
15 | *
16 | * @param value The value to test
17 | */
18 | function isObject(value: any) {
19 | return value !== null && typeof value === "object";
20 | }
21 |
22 | /**
23 | * Returns true if an object was created by the `Object` constructor, or Object.create(null).
24 | *
25 | * @param o The object to test
26 | *
27 | * @example
28 | * ```ts
29 | * isPlainObject(Object.create({})); // => true
30 | * isPlainObject(Object.create(Object.prototype)); // => true
31 | * isPlainObject({foo: "bar"}); // => true
32 | * isPlainObject({}); // => true
33 | * isPlainObject(Object.create(null)); // => true
34 | *
35 | * isPlainObject(["foo", "bar"]); // => false
36 | * isPlainObject([]); // => false
37 | * isPlainObject(new Foo); // => false
38 | * ```
39 | */
40 | export function isPlainObject(o: object): o is Record {
41 | if (isObject(o) === false) return false;
42 |
43 | // If has modified constructor
44 | const ctor = o.constructor;
45 | if (ctor === undefined) return true;
46 |
47 | // If has modified prototype
48 | const prot = ctor.prototype;
49 | if (isObject(prot) === false) return false;
50 |
51 | // If constructor does not have an Object-specific method
52 | if (prot.hasOwnProperty("isPrototypeOf") === false) {
53 | return false;
54 | }
55 |
56 | // Most likely a plain Object
57 | return true;
58 | }
59 |
60 | if (import.meta.vitest) {
61 | const { describe, it, expect } = await import("vitest");
62 |
63 | describe("isPlainObject", () => {
64 | it("returns true for plain objects", () => {
65 | expect(isPlainObject(Object.create({}))).toBe(true);
66 | expect(isPlainObject(Object.create(Object.prototype))).toBe(true);
67 | expect(isPlainObject({ foo: "bar" })).toBe(true);
68 | expect(isPlainObject({})).toBe(true);
69 | expect(isPlainObject(Object.create(null))).toBe(true);
70 | });
71 |
72 | it("returns false for non-plain objects", () => {
73 | expect(isPlainObject(["foo", "bar"])).toBe(false);
74 | expect(isPlainObject([])).toBe(false);
75 | expect(isPlainObject(new Date())).toBe(false);
76 | // @ts-expect-error
77 | expect(isPlainObject(null)).toBe(false);
78 | // @ts-expect-error
79 | expect(isPlainObject(1)).toBe(false);
80 | });
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/apps/stories/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 | # Storybook build outputs
133 | storybook-static
134 |
--------------------------------------------------------------------------------
/packages/tmdb-api/src/endpoints.ts:
--------------------------------------------------------------------------------
1 | export const ENDPOINTS = {
2 | account: {
3 | details: ["GET /account/{id}?session_id={session_id}", {}],
4 | addFavorite: ["POST /account/{id}/favorite?session_id={session_id}", {}],
5 | addWatchlist: ["POST /account/{id}/watchlist?session_id={session_id}", {}],
6 | favoriteMovies: [
7 | "GET /account/{id}/favorite/movies?language={language}&page={page}&session_id={session_id}&sort_by={sort_by}",
8 | { language: "en-US", page: 1, sort_by: "created_at.asc" },
9 | ],
10 | favoriteTV: [
11 | "GET /account/{id}/favorite/tv?language={language}&page={page}&session_id={session_id}&sort_by={sort_by}",
12 | { language: "en-US", page: 1, sort_by: "created_at.asc" },
13 | ],
14 | lists: [
15 | "GET /account/{id}/lists?page={page}&session_id={session_id}",
16 | { page: 1 },
17 | ],
18 | ratedMovies: [
19 | "GET /account/{id}/rated/movies?language={language}&page={page}&session_id={session_id}&sort_by={sort_by}",
20 | { language: "en-US", page: 1, sort_by: "created_at.asc" },
21 | ],
22 | ratedTV: [
23 | "GET /account/{id}/rated/tv?language={language}&page={page}&session_id={session_id}&sort_by={sort_by}",
24 | { language: "en-US", page: 1, sort_by: "created_at.asc" },
25 | ],
26 | ratedTVEpisodes: [
27 | "GET /account/{id}/rated/tv/episodes?language={language}&page={page}&session_id={session_id}&sort_by={sort_by}",
28 | { language: "en-US", page: 1, sort_by: "created_at.asc" },
29 | ],
30 | watchlistMovies: [
31 | "GET /account/{id}/watchlist/movies?language={language}&page={page}&session_id={session_id}&sort_by={sort_by}",
32 | { language: "en-US", page: 1, sort_by: "created_at.asc" },
33 | ],
34 | watchlistTV: [
35 | "GET /account/{id}/watchlist/tv?language={language}&page={page}&session_id={session_id}&sort_by={sort_by}",
36 | { language: "en-US", page: 1, sort_by: "created_at.asc" },
37 | ],
38 | },
39 | movie: {
40 | details: [
41 | "GET /movie/{id}?append_to_response={append_to_response}&language={language}",
42 | { language: "en-US" },
43 | ],
44 | },
45 | search: {
46 | multi: [
47 | "GET /search/multi?query={query}&include_adult={include_adult}&language={language}&page={page}",
48 | { include_adult: false, language: "en-US", page: 1 },
49 | ],
50 | movie: [
51 | "GET /search/movie?query={query}&include_adult={include_adult}&language={language}&primary_release_year={primary_release_year}&page={page}®ion={region}&year={year}",
52 | { include_adult: false, language: "en-US", page: 1 },
53 | ],
54 | tv: [
55 | "GET /search/tv?query={query}&first_air_date_year={first_air_date_year}&include_adult={include_adult}&language={language}&page={page}&year={year}",
56 | { include_adult: false, language: "en-US", page: 1 },
57 | ],
58 | },
59 | } as const;
60 |
--------------------------------------------------------------------------------
/packages/tmdb-request/src/parse.ts:
--------------------------------------------------------------------------------
1 | import type { Context, DefaultParams } from "@/defaults";
2 | import { mergeDeep } from "@/merge-deep";
3 | import { splitObj } from "@/split-obj";
4 | import { parseTemplate, type Template } from "url-template";
5 |
6 | export function parse(defaults: DefaultParams) {
7 | return (route?: string, opts: Options = {}): Context => {
8 | // replace :varname with {varname} to make it RFC 6570 compatible
9 | const _route = (route || "/").replace(/:([a-z]\w+)/g, "{$1}");
10 | const [_method, path] = _route.trim().split(" ") as [string, string?];
11 | const [params, options] = splitParams(mergeDeep(defaults, opts));
12 |
13 | const method = path ? _method.toUpperCase() : defaults.method;
14 | const url = parseTemplate(path ?? _method).expand(options);
15 | const body = method === "POST" ? params.body : defaults.body;
16 |
17 | return Object.assign(params, { method, url, body });
18 | };
19 | }
20 |
21 | function splitParams(opts: Options): [Context, ExpandParams] {
22 | return splitObj(opts, ["method", "headers", "baseUrl", "url", "body"]);
23 | }
24 |
25 | /** Any object that can be expanded into a URL */
26 | type ExpandParams = Parameters[0];
27 | /** The `opts` parameter of {@link request} and {@link parser}.*/
28 | export type Options = DefaultParams & ExpandParams;
29 |
30 | if (import.meta.vitest) {
31 | const { describe, it, expect } = await import("vitest");
32 | const { DEFAULTS } = await import("@/defaults");
33 |
34 | describe("splitParams", () => {
35 | it("should split params", () => {
36 | const [params, options] = splitParams(DEFAULTS);
37 |
38 | expect(params.method).toEqual("GET");
39 | delete params.headers?.["user-agent"]; // Eliminate environmental impacts
40 | expect(params.headers).toEqual({ accept: "application/json" });
41 | expect(params.url).toBeUndefined();
42 | expect(options).toEqual({});
43 | });
44 | });
45 |
46 | describe("parse", () => {
47 | const parser = parse(DEFAULTS);
48 |
49 | it("should parse route", () => {
50 | expect(typeof parser).toBe("function");
51 |
52 | const getCtx = parser("/foo/{bar}", { body: "foo", bar: "baz" });
53 |
54 | expect(getCtx.url).toEqual("/foo/baz");
55 | expect(getCtx.body).toBeNull();
56 | expect(getCtx.method).toEqual("GET");
57 |
58 | const postCtx = parser("POST /foo/{bar}", { body: "foo", bar: "baz" });
59 |
60 | expect(postCtx.url).toEqual("/foo/baz");
61 | expect(postCtx.body).toEqual("foo");
62 | expect(postCtx.method).toEqual("POST");
63 |
64 | expect(parser().url).toEqual("/");
65 | });
66 |
67 | it("should parse route with custom baseUrl", () => {
68 | expect(typeof parser).toBe("function");
69 |
70 | expect(parser("/foo/{bar}", { bar: "baz" }).baseUrl).toEqual(
71 | "https://api.themoviedb.org/3"
72 | );
73 |
74 | expect(
75 | parser("/foo/{bar}", { bar: "baz", baseUrl: "https://foo.bar" }).baseUrl
76 | ).toEqual("https://foo.bar");
77 | });
78 |
79 | it("should replace :varname with {varname}", () => {
80 | expect(parser("/foo/:bar", { bar: "baz" }).url).toEqual("/foo/baz");
81 | });
82 | });
83 | }
84 |
--------------------------------------------------------------------------------
/apps/stories/shadcn-ui/card.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 | import { LuBellRing, LuCheck } from "react-icons/lu";
3 |
4 | import { cn } from "@/lib/utils";
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardFooter,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card";
14 | import { Switch } from "@/components/ui/switch";
15 |
16 | export default {
17 | title: "Shadcn-ui/Card",
18 | component: Card,
19 | argTypes: {
20 | className: { control: { type: "text" } },
21 | },
22 | } as Meta;
23 | type Story = StoryObj;
24 |
25 | export const Default: Story = {
26 | render: ({ className, ...props }) => (
27 |
28 |
29 | Card Title
30 | Card Description
31 |
32 |
33 | Card Content
34 |
35 |
36 | Card Footer
37 |
38 |
39 | ),
40 | };
41 |
42 | const notifications = [
43 | {
44 | title: "Your call has been confirmed.",
45 | description: "1 hour ago",
46 | },
47 | {
48 | title: "You have a new message!",
49 | description: "1 hour ago",
50 | },
51 | {
52 | title: "Your subscription is expiring soon!",
53 | description: "2 hours ago",
54 | },
55 | ];
56 |
57 | export const Notification: Story = {
58 | render: ({ className, ...props }) => (
59 |
60 |
61 | Notifications
62 | You have 3 unread messages.
63 |
64 |
65 |
66 |
67 |
68 |
69 | Push Notifications
70 |
71 |
72 | Send notifications to device.
73 |
74 |
75 |
76 |
77 |
78 | {notifications.map((notification, index) => (
79 |
83 |
84 |
85 |
86 | {notification.title}
87 |
88 |
89 | {notification.description}
90 |
91 |
92 |
93 | ))}
94 |
95 |
96 |
97 |
100 |
101 |
102 | ),
103 | };
104 |
--------------------------------------------------------------------------------
/apps/web/app/search/components/pagination.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback, useMemo } from "react";
4 | import Link from "next/link";
5 | import { usePathname, useSearchParams } from "next/navigation";
6 | import { LuChevronLeft, LuChevronRight } from "react-icons/lu";
7 |
8 | import { cn, range } from "@/lib/utils";
9 | import { Button, buttonVariants } from "@/components/ui/button";
10 |
11 | export const Pagination: React.FC<{
12 | totalPages: number;
13 | }> = ({ totalPages }) => {
14 | const searchParams = useSearchParams()!;
15 | const pathname = usePathname();
16 | const currentPage = Number(searchParams.get("page") ?? 1);
17 | const createQueryString = useCallback(
18 | (page: number) => {
19 | const params = new URLSearchParams(searchParams.toString());
20 | params.set("page", page.toString());
21 | return params.toString();
22 | },
23 | [searchParams]
24 | );
25 | const createPagination = useCallback(
26 | (start: number, end: number) => {
27 | return range(start, end + 1).map((page) => (
28 |
41 | ));
42 | },
43 | [currentPage, pathname, createQueryString]
44 | );
45 | const pagination = useMemo(() => {
46 | if (totalPages <= 9) return createPagination(1, totalPages);
47 | if (currentPage < 4 || currentPage > totalPages - 4) {
48 | return (
49 | <>
50 | {createPagination(1, 4)}
51 |
52 | {createPagination(totalPages - 3, totalPages)}
53 | >
54 | );
55 | }
56 | return (
57 | <>
58 | {createPagination(1, 2)}
59 |
60 | {createPagination(currentPage - 1, currentPage + 1)}
61 |
62 | {createPagination(totalPages - 1, totalPages)}
63 | >
64 | );
65 | }, [currentPage, totalPages, createPagination]);
66 |
67 | return (
68 |
97 | );
98 | };
99 |
100 | const Ellipsis: React.FC = () => (
101 |
107 | ...
108 |
109 | );
110 |
--------------------------------------------------------------------------------
/apps/web/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # movisea-web
2 |
3 | ## 0.0.13
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies [[`02c2203`](https://github.com/mogeko/movisea/commit/02c22031e9f0fd9d286abfc51c6f125eb6914090), [`14fc3a4`](https://github.com/mogeko/movisea/commit/14fc3a4829d4602b5935ced24ea13c3e451f4c04), [`9803a6e`](https://github.com/mogeko/movisea/commit/9803a6e53cc802d2bb0ca43ffd3ec657eabeca8d)]:
8 | - @mogeko/tmdb-request@1.3.2
9 |
10 | ## 0.0.12
11 |
12 | ### Patch Changes
13 |
14 | - Updated dependencies [[`772e193`](https://github.com/mogeko/movisea/commit/772e193647ab4f1aeb405e6be250de1b197914cf), [`040bc76`](https://github.com/mogeko/movisea/commit/040bc768e8934c4e0bbb955c3e12340b2da41784)]:
15 | - @mogeko/tmdb-request@1.3.1
16 |
17 | ## 0.0.11
18 |
19 | ### Patch Changes
20 |
21 | - Updated dependencies [[`f1edd70`](https://github.com/mogeko/movisea/commit/f1edd705fa368d3d5dbc5f4cfbbff93c4c972abd), [`67fad93`](https://github.com/mogeko/movisea/commit/67fad93720aa6d4716eaa5a312c11823152dec78), [`0c0916b`](https://github.com/mogeko/movisea/commit/0c0916bce1db46eee04f0dc5802fcb280294beb5), [`aa1fa17`](https://github.com/mogeko/movisea/commit/aa1fa176ffb4a14513b9b6dd1809e89f0b4d81b6)]:
22 | - @mogeko/tmdb-request@1.3.0
23 |
24 | ## 0.0.10
25 |
26 | ### Patch Changes
27 |
28 | - [#41](https://github.com/mogeko/movisea/pull/41) [`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774) Thanks [@mogeko](https://github.com/mogeko)! - Upgrade dependencies
29 |
30 | - bump `eslint` from `8.41.0` to `8.44.0` ([#38](https://github.com/mogeko/movisea/pull/38))
31 | - bump `@types/node` from `20.2.5` to `20.3.3` ([#37](https://github.com/mogeko/movisea/pull/37))
32 | - bump `@tsconfig/next` from `1.0.5` to `2.0.0` ([#36](https://github.com/mogeko/movisea/pull/36))
33 |
34 | - Updated dependencies [[`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774)]:
35 | - @mogeko/tmdb-request@1.2.5
36 |
37 | ## 0.0.9
38 |
39 | ### Patch Changes
40 |
41 | - Updated dependencies [[`8a7c476`](https://github.com/mogeko/movisea/commit/8a7c4767fc817e495792e1ce99fbc12e6f4722b5)]:
42 | - @mogeko/tmdb-request@1.2.4
43 |
44 | ## 0.0.8
45 |
46 | ### Patch Changes
47 |
48 | - Updated dependencies [[`f02efa6`](https://github.com/mogeko/movisea/commit/f02efa69403ef02284b49ff0e0e7b050a9b4c99c)]:
49 | - tmdb-request@1.2.3
50 |
51 | ## 0.0.7
52 |
53 | ### Patch Changes
54 |
55 | - Updated dependencies [[`a87825e`](https://github.com/mogeko/movisea/commit/a87825e9ee8de8e817d21ac09c6b23612c07c48c), [`a87825e`](https://github.com/mogeko/movisea/commit/a87825e9ee8de8e817d21ac09c6b23612c07c48c)]:
56 | - tmdb-request@1.2.2
57 |
58 | ## 0.0.6
59 |
60 | ### Patch Changes
61 |
62 | - Updated dependencies [[`0bfb3b1`](https://github.com/mogeko/movisea/commit/0bfb3b19ee76fcc89d33d9e200be815e50f60848), [`18c06db`](https://github.com/mogeko/movisea/commit/18c06db12b40056c4f287046e89a2117b704f6e8), [`3d13fcc`](https://github.com/mogeko/movisea/commit/3d13fcc1b9456b45aba9026fc7621caae711182d)]:
63 | - tmdb-request@1.2.1
64 |
65 | ## 0.0.5
66 |
67 | ### Patch Changes
68 |
69 | - Updated dependencies [[`3698e8d`](https://github.com/mogeko/movisea/commit/3698e8dfcb77f465519b84287ca95c464106d048)]:
70 | - tmdb-request@1.2.0
71 |
72 | ## 0.0.4
73 |
74 | ### Patch Changes
75 |
76 | - Updated dependencies [[`9393959`](https://github.com/mogeko/movisea/commit/9393959f8e7fcba6fc3c9d5d23713655863d9bbd)]:
77 | - tmdb-request@1.1.1
78 |
79 | ## 0.0.3
80 |
81 | ### Patch Changes
82 |
83 | - Updated dependencies [[`73e6e90`](https://github.com/mogeko/movisea/commit/73e6e9075ee8bd28bf10bfbd255cf7d43c56e0ca), [`2837897`](https://github.com/mogeko/movisea/commit/2837897af7d5c3b3396601ec1534f7ee86333215), [`f9ec3ad`](https://github.com/mogeko/movisea/commit/f9ec3adb187a7642a85db9a28c4ffe0284bbd7d6)]:
84 | - tmdb-request@1.1.0
85 |
86 | ## 0.0.2
87 |
88 | ### Patch Changes
89 |
90 | - Updated dependencies []:
91 | - tmdb-request@1.0.0
92 |
--------------------------------------------------------------------------------
/apps/web/app/search/[type]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { request } from "@mogeko/tmdb-request";
3 |
4 | import { tokens } from "@/config/tokens";
5 | import type { XOR } from "@/lib/utils";
6 | import { Separator } from "@/components/ui/separator";
7 | import { PosterImage } from "@/components/poster-image";
8 | import { Pagination } from "@/app/search/components/pagination";
9 |
10 | const SearchPage: React.FC<{
11 | searchParams: { q: string; page?: number };
12 | params: { type: string };
13 | }> = async ({ searchParams: { q, page }, params }) => {
14 | const data = await getSearchInfo(params.type, {
15 | page: page?.toString(),
16 | query: q,
17 | });
18 |
19 | return (
20 |
21 |
22 |
23 | {data.total_results} media results
24 |
25 |
26 | {data.results.map((result) => (
27 |
28 |
29 |
33 |
39 |
40 |
41 | {result.title ?? result.name}
42 |
43 |
44 | {new Date(
45 | result.release_date ?? result.first_air_date
46 | ).toLocaleDateString()}
47 |
48 |
49 | {result.overview}
50 |
51 |
52 |
53 |
54 | ))}
55 |
56 |
57 | );
58 | };
59 |
60 | const getSearchInfo = async (type: string, params: SearchParams) => {
61 | return await request(
62 | "/search/{type}?query={query}&include_adult={include_adult}&language={language}&page={page}",
63 | {
64 | headers: { authorization: `Bearer ${tokens.tmdb}` },
65 | query: params.query,
66 | include_adult: params.include_adult ?? "false",
67 | language: params.language ?? "en-US",
68 | page: params.page ?? "1",
69 | type: type,
70 | }
71 | );
72 | };
73 |
74 | type SearchParams = {
75 | query: string;
76 | include_adult?: "false" | "true";
77 | language?: string;
78 | page?: string;
79 | };
80 |
81 | export type SearchInfo = {
82 | page: number;
83 | results: Array<
84 | XOR<
85 | {
86 | media_type: "movie";
87 | title: string;
88 | original_title: string;
89 | release_date: string;
90 | },
91 | {
92 | media_type: "tv";
93 | name: string;
94 | original_name: string;
95 | first_air_date: string;
96 | origin_country: string[];
97 | }
98 | > & {
99 | ault: boolean;
100 | backdrop_path: string;
101 | id: number;
102 | original_language: string;
103 | overview: string;
104 | poster_path: string;
105 | genre_ids: number[];
106 | popularity: number;
107 | origin_country: string[];
108 | vote_average: number;
109 | vote_count: number;
110 | }
111 | >;
112 | total_pages: number;
113 | total_results: number;
114 | };
115 |
116 | export default SearchPage;
117 |
--------------------------------------------------------------------------------
/apps/stories/shadcn-ui/input.stories.tsx:
--------------------------------------------------------------------------------
1 | import { zodResolver } from "@hookform/resolvers/zod";
2 | import type { Meta, StoryObj } from "@storybook/react";
3 | import { useForm } from "react-hook-form";
4 | import * as z from "zod";
5 |
6 | import { toast } from "@/lib/use-toast";
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | Form,
10 | FormControl,
11 | FormDescription,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form";
17 | import { Input } from "@/components/ui/input";
18 | import { Label } from "@/components/ui/label";
19 |
20 | export default {
21 | title: "Shadcn-ui/Input",
22 | component: Input,
23 | argTypes: {
24 | type: {
25 | options: ["text", "search", "email", "password", "number", "file"],
26 | control: { type: "select" },
27 | },
28 | disabled: { control: { type: "boolean" } },
29 | },
30 | } as Meta;
31 | type Story = StoryObj;
32 |
33 | export const Search: Story = {
34 | args: {
35 | placeholder: "Search...",
36 | type: "search",
37 | },
38 | };
39 |
40 | export const Disable: Story = {
41 | args: {
42 | disabled: true,
43 | ...Search.args,
44 | },
45 | };
46 |
47 | export const WithButton: Story = {
48 | render: (args) => (
49 |
50 |
51 |
52 |
53 | ),
54 | args: {
55 | placeholder: "Email address",
56 | type: "email",
57 | },
58 | };
59 |
60 | export const WithLabel: Story = {
61 | render: ({ id, ...props }) => (
62 |
63 |
64 |
65 |
Enter your email address.
66 |
67 | ),
68 | args: {
69 | placeholder: "Email address",
70 | type: "email",
71 | },
72 | };
73 |
74 | export const File: Story = {
75 | render: ({ id, ...props }) => (
76 |
77 |
78 |
79 |
80 | ),
81 | args: {
82 | id: "picture",
83 | type: "file",
84 | },
85 | };
86 |
87 | const schema = z.object({
88 | username: z.string().min(2, {
89 | message: "Username must be at least 2 characters.",
90 | }),
91 | });
92 |
93 | const InputInForm: React.FC = (_args) => {
94 | const form = useForm>({
95 | resolver: zodResolver(schema),
96 | });
97 |
98 | function onSubmit(data: z.infer) {
99 | toast({
100 | title: "You submitted the following values:",
101 | description: (
102 |
103 | {JSON.stringify(data, null, 2)}
104 |
105 | ),
106 | });
107 | }
108 |
109 | return (
110 |
130 |
131 | );
132 | };
133 |
134 | export const Demo1: Story = {
135 | render: () => ,
136 | parameters: {
137 | controls: {
138 | hideNoControlsWarning: true,
139 | exclude: /type|disabled/g,
140 | },
141 | },
142 | };
143 |
--------------------------------------------------------------------------------
/apps/web/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { LuX } from "react-icons/lu";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: DialogPrimitive.DialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | );
24 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
25 |
26 | const DialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ));
39 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
40 |
41 | const DialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
46 |
47 |
55 | {children}
56 |
57 |
58 | Close
59 |
60 |
61 |
62 | ));
63 | DialogContent.displayName = DialogPrimitive.Content.displayName;
64 |
65 | const DialogHeader = ({
66 | className,
67 | ...props
68 | }: React.HTMLAttributes) => (
69 |
76 | );
77 | DialogHeader.displayName = "DialogHeader";
78 |
79 | const DialogFooter = ({
80 | className,
81 | ...props
82 | }: React.HTMLAttributes) => (
83 |
90 | );
91 | DialogFooter.displayName = "DialogFooter";
92 |
93 | const DialogTitle = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, ...props }, ref) => (
97 |
105 | ));
106 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
107 |
108 | const DialogDescription = React.forwardRef<
109 | React.ElementRef,
110 | React.ComponentPropsWithoutRef
111 | >(({ className, ...props }, ref) => (
112 |
117 | ));
118 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
119 |
120 | export {
121 | Dialog,
122 | DialogTrigger,
123 | DialogContent,
124 | DialogHeader,
125 | DialogFooter,
126 | DialogTitle,
127 | DialogDescription,
128 | };
129 |
--------------------------------------------------------------------------------
/packages/tmdb-request/src/defaults.ts:
--------------------------------------------------------------------------------
1 | import { getUserAgent } from "universal-user-agent";
2 |
3 | export const DEFAULTS = {
4 | method: "GET",
5 | baseUrl: "https://api.themoviedb.org/3",
6 | headers: {
7 | accept: "application/json",
8 | "user-agent": getUserAgent(),
9 | },
10 | body: null,
11 | } as const;
12 |
13 | /**
14 | * The type for parameters of {@link parse}. It will set the default
15 | * behavior of {@link parser} and {@link request}.
16 | *
17 | * @remarks
18 | * The signature of this type is same as {@link Context} but with
19 | * optional properties.
20 | */
21 | export type DefaultParams = {
22 | /**
23 | * The HTTP method for the request.
24 | *
25 | * @remarks
26 | * HTTP defines a set of request methods to indicate the desired action to be
27 | * performed for a given resource.
28 | *
29 | * In this case, the only methods supported by TMDB are `GET`, `POST`, and `DELETE`.
30 | *
31 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods}
32 | *
33 | * @defaultValue "GET"
34 | */
35 | method?: "GET" | "POST" | "DELETE";
36 | /**
37 | * The `headers` for the request.
38 | *
39 | * @remarks
40 | * HTTP `headers` let the client and the server pass additional information with
41 | * an HTTP request or response.
42 | *
43 | * @see {@link https://developer.mozilla.org/en-US/docs/web/http/headers}
44 | */
45 | headers?: {
46 | /**
47 | * The `Accept` header for the request.
48 | *
49 | * @remarks
50 | * The `Accept` request HTTP header indicates which content types, expressed as MIME types,
51 | * the client is able to understand.
52 | *
53 | * For TMDB API, the only response format we support is JSON.
54 | *
55 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept}
56 | * @see {@link https://developer.themoviedb.org/docs/json-and-jsonp}
57 | *
58 | * @defaultValue "application/json"
59 | */
60 | accept?: "application/json";
61 | /**
62 | * The `Content-Type` header for the request with `POST` and `DELETE` methods.
63 | *
64 | * @remarks
65 | * The Content-Type representation header is used to indicate the original media type of the resource
66 | * (prior to any content encoding applied for sending).
67 | *
68 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type}
69 | */
70 | "content-type"?: "application/json" | "application/json;charset=utf-8";
71 | /**
72 | * The `Authorization` header for the request.
73 | *
74 | * @remarks
75 | * The HTTP `Authorization` request header can be used to provide credentials that authenticate
76 | * a user agent with a server, allowing access to a protected resource.
77 | *
78 | * In here, it should be a string that starts with `Bearer `.
79 | *
80 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization}
81 | */
82 | authorization?: `Bearer ${string}`;
83 | /**
84 | * The `User-Agent` header for the request.
85 | *
86 | * @remarks
87 | * The `User-Agent` request header is a characteristic string that lets servers and network peers
88 | * identify the application, operating system, vendor, and/or version of the requesting user agent.
89 | *
90 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent}
91 | */
92 | "user-agent"?: string;
93 | /** other headers */
94 | [key: string]: any;
95 | };
96 | /**
97 | * The base URL for the request.
98 | *
99 | * @defaultValue "https://api.themoviedb.org/3"
100 | */
101 | baseUrl?: `https://${string}` | `http://${string}`;
102 | /**
103 | * The URL (relative to the {@link baseUrl}) for the request.
104 | *
105 | * @remarks
106 | * It has to be a string consisting of `URL Template` and the request method, e.g. `GET /movie/{id}`.
107 | * If it’s set to a pure URL, only the method defaults to `GET`.
108 | *
109 | * @see {@link https://www.rfc-editor.org/rfc/rfc6570}
110 | */
111 | url?: `/${string}` | `${Context["method"]} /${string}`;
112 | /**
113 | * The `body` for the request.
114 | *
115 | * @remarks
116 | * The `body` of a request is the data sent by the client to your API.
117 | *
118 | * It will be sent as-is in a `POST` request. For `GET` and `DELETE` request, it will always be `null`.
119 | *
120 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/body}
121 | *
122 | * @defaultValue null
123 | */
124 | body?: BodyInit | null;
125 | };
126 |
127 | /** The result of {@link parser}. */
128 | export type Context = Required;
129 |
--------------------------------------------------------------------------------
/apps/web/lib/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react";
3 |
4 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
5 |
6 | const TOAST_LIMIT = 1;
7 | const TOAST_REMOVE_DELAY = 1000000;
8 |
9 | type ToasterToast = ToastProps & {
10 | id: string;
11 | title?: React.ReactNode;
12 | description?: React.ReactNode;
13 | action?: ToastActionElement;
14 | };
15 |
16 | const actionTypes = {
17 | ADD_TOAST: "ADD_TOAST",
18 | UPDATE_TOAST: "UPDATE_TOAST",
19 | DISMISS_TOAST: "DISMISS_TOAST",
20 | REMOVE_TOAST: "REMOVE_TOAST",
21 | } as const;
22 |
23 | let count = 0;
24 |
25 | function genId() {
26 | count = (count + 1) % Number.MAX_VALUE;
27 | return count.toString();
28 | }
29 |
30 | type ActionType = typeof actionTypes;
31 |
32 | type Action =
33 | | {
34 | type: ActionType["ADD_TOAST"];
35 | toast: ToasterToast;
36 | }
37 | | {
38 | type: ActionType["UPDATE_TOAST"];
39 | toast: Partial;
40 | }
41 | | {
42 | type: ActionType["DISMISS_TOAST"];
43 | toastId?: ToasterToast["id"];
44 | }
45 | | {
46 | type: ActionType["REMOVE_TOAST"];
47 | toastId?: ToasterToast["id"];
48 | };
49 |
50 | interface State {
51 | toasts: ToasterToast[];
52 | }
53 |
54 | const toastTimeouts = new Map>();
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return;
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId);
63 | dispatch({
64 | type: "REMOVE_TOAST",
65 | toastId: toastId,
66 | });
67 | }, TOAST_REMOVE_DELAY);
68 |
69 | toastTimeouts.set(toastId, timeout);
70 | };
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case "ADD_TOAST":
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | };
79 |
80 | case "UPDATE_TOAST":
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) =>
84 | t.id === action.toast.id ? { ...t, ...action.toast } : t
85 | ),
86 | };
87 |
88 | case "DISMISS_TOAST": {
89 | const { toastId } = action;
90 |
91 | // ! Side effects ! - This could be extracted into a dismissToast() action,
92 | // but I'll keep it here for simplicity
93 | if (toastId) {
94 | addToRemoveQueue(toastId);
95 | } else {
96 | state.toasts.forEach((toast) => {
97 | addToRemoveQueue(toast.id);
98 | });
99 | }
100 |
101 | return {
102 | ...state,
103 | toasts: state.toasts.map((t) =>
104 | t.id === toastId || toastId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t
110 | ),
111 | };
112 | }
113 | case "REMOVE_TOAST":
114 | if (action.toastId === undefined) {
115 | return {
116 | ...state,
117 | toasts: [],
118 | };
119 | }
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | };
124 | }
125 | };
126 |
127 | const listeners: Array<(state: State) => void> = [];
128 |
129 | let memoryState: State = { toasts: [] };
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action);
133 | listeners.forEach((listener) => {
134 | listener(memoryState);
135 | });
136 | }
137 |
138 | type Toast = Omit;
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId();
142 |
143 | const update = (props: ToasterToast) =>
144 | dispatch({
145 | type: "UPDATE_TOAST",
146 | toast: { ...props, id },
147 | });
148 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
149 |
150 | dispatch({
151 | type: "ADD_TOAST",
152 | toast: {
153 | ...props,
154 | id,
155 | open: true,
156 | onOpenChange: (open) => {
157 | if (!open) dismiss();
158 | },
159 | },
160 | });
161 |
162 | return {
163 | id: id,
164 | dismiss,
165 | update,
166 | };
167 | }
168 |
169 | function useToast() {
170 | const [state, setState] = React.useState(memoryState);
171 |
172 | React.useEffect(() => {
173 | listeners.push(setState);
174 | return () => {
175 | const index = listeners.indexOf(setState);
176 | if (index > -1) {
177 | listeners.splice(index, 1);
178 | }
179 | };
180 | }, [state]);
181 |
182 | return {
183 | ...state,
184 | toast,
185 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186 | };
187 | }
188 |
189 | export { useToast, toast };
190 |
--------------------------------------------------------------------------------
/apps/web/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as LabelPrimitive from "@radix-ui/react-label";
3 | import { Slot } from "@radix-ui/react-slot";
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form";
12 |
13 | import { cn } from "@/lib/utils";
14 | import { Label } from "@/components/ui/label";
15 |
16 | const Form = FormProvider;
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = { name: TName };
22 |
23 | const FormFieldContext = React.createContext(
24 | {} as FormFieldContextValue
25 | );
26 |
27 | const FormField = <
28 | TFieldValues extends FieldValues = FieldValues,
29 | TName extends FieldPath = FieldPath
30 | >({
31 | ...props
32 | }: ControllerProps) => {
33 | return (
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | const useFormField = () => {
41 | const fieldContext = React.useContext(FormFieldContext);
42 | const itemContext = React.useContext(FormItemContext);
43 | const { getFieldState, formState } = useFormContext();
44 |
45 | const fieldState = getFieldState(fieldContext.name, formState);
46 |
47 | if (!fieldContext) {
48 | throw new Error("useFormField should be used within ");
49 | }
50 |
51 | const { id } = itemContext;
52 |
53 | return {
54 | id,
55 | name: fieldContext.name,
56 | formItemId: `${id}-form-item`,
57 | formDescriptionId: `${id}-form-item-description`,
58 | formMessageId: `${id}-form-item-message`,
59 | ...fieldState,
60 | };
61 | };
62 |
63 | type FormItemContextValue = { id: string };
64 |
65 | const FormItemContext = React.createContext(
66 | {} as FormItemContextValue
67 | );
68 |
69 | const FormItem = React.forwardRef<
70 | HTMLDivElement,
71 | React.HTMLAttributes
72 | >(({ className, ...props }, ref) => {
73 | const id = React.useId();
74 |
75 | return (
76 |
77 |
78 |
79 | );
80 | });
81 | FormItem.displayName = "FormItem";
82 |
83 | const FormLabel = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => {
87 | const { error, formItemId } = useFormField();
88 |
89 | return (
90 |
96 | );
97 | });
98 | FormLabel.displayName = "FormLabel";
99 |
100 | const FormControl = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ ...props }, ref) => {
104 | const { error, formItemId, formDescriptionId, formMessageId } =
105 | useFormField();
106 |
107 | return (
108 |
119 | );
120 | });
121 | FormControl.displayName = "FormControl";
122 |
123 | const FormDescription = React.forwardRef<
124 | HTMLParagraphElement,
125 | React.HTMLAttributes
126 | >(({ className, ...props }, ref) => {
127 | const { formDescriptionId } = useFormField();
128 |
129 | return (
130 |
136 | );
137 | });
138 | FormDescription.displayName = "FormDescription";
139 |
140 | const FormMessage = React.forwardRef<
141 | HTMLParagraphElement,
142 | React.HTMLAttributes
143 | >(({ className, children, ...props }, ref) => {
144 | const { error, formMessageId } = useFormField();
145 | const body = error ? String(error?.message) : children;
146 |
147 | if (!body) return null;
148 |
149 | return (
150 |
156 | {body}
157 |
158 | );
159 | });
160 | FormMessage.displayName = "FormMessage";
161 |
162 | export {
163 | useFormField,
164 | Form,
165 | FormItem,
166 | FormLabel,
167 | FormControl,
168 | FormDescription,
169 | FormMessage,
170 | FormField,
171 | };
172 |
--------------------------------------------------------------------------------
/apps/web/components/search.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback, useEffect, useState } from "react";
4 | import type { Dispatch, SetStateAction } from "react";
5 | import { usePathname, useRouter } from "next/navigation";
6 | import { useTheme } from "next-themes";
7 | import {
8 | LuCornerDownRight,
9 | LuLaptop,
10 | LuMoon,
11 | LuSearch,
12 | LuSunMedium,
13 | LuTrash2,
14 | } from "react-icons/lu";
15 |
16 | import { useLocalStorage } from "@/lib/use-localstorage";
17 | import { cn } from "@/lib/utils";
18 | import { Button } from "@/components/ui/button";
19 | import {
20 | CommandDialog,
21 | CommandGroup,
22 | CommandInput,
23 | CommandItem,
24 | CommandList,
25 | CommandSeparator,
26 | } from "@/components/ui/command";
27 |
28 | export const SearchInHeader: React.FC = () => {
29 | const pathname = usePathname();
30 |
31 | if (pathname === "/") return ;
32 | return ;
33 | };
34 |
35 | export const Search: React.FC<{ className?: string }> = ({ className }) => {
36 | const [open, setOpen] = useState(false);
37 | const [searchHistory, setSearchHistory] = useSearchHistory();
38 | const [search, setSearch] = useState("");
39 | const router = useRouter();
40 | const handleSubmit = useCallback(
41 | (value: string) => {
42 | if (value !== "") {
43 | setSearchHistory((history) => {
44 | return !history.includes(value) ? [value, ...history] : history;
45 | });
46 | setSearch(""), setOpen(false);
47 |
48 | router.push(`/search/multi?q=${value}`);
49 | }
50 | },
51 | [setSearchHistory, setSearch, setOpen, router]
52 | );
53 | const { setTheme } = useTheme();
54 |
55 | useEffect(() => {
56 | const down = (e: KeyboardEvent) => {
57 | if (e.key === "k" && e.metaKey) {
58 | setOpen((open) => !open);
59 | }
60 | };
61 | window.addEventListener("keydown", down);
62 | return () => window.removeEventListener("keydown", down);
63 | }, []);
64 |
65 | return (
66 | <>
67 |
80 |
81 |
85 |
86 | {search !== "" && (
87 |
88 |
89 | Search ”{search}” on TMDB...
90 |
91 | )}
92 |
93 | {searchHistory.map((keyword, i) => (
94 |
99 | {keyword}
100 |
101 | ))}
102 | setSearchHistory([])}>
103 |
104 | Clear search history
105 |
106 |
107 |
108 |
109 | setTheme("light")}>
110 |
111 | Light
112 |
113 | setTheme("dark")}>
114 |
115 | Dark
116 |
117 | setTheme("system")}>
118 |
119 | System
120 |
121 |
122 |
123 |
124 | >
125 | );
126 | };
127 |
128 | const useSearchHistory = (sKey = "searchHistory", init: string[] = []) => {
129 | const [item, setItem] = useLocalStorage(sKey, JSON.stringify(init));
130 | const setSearchHistory: Dispatch> = (history) => {
131 | if (typeof history === "function") {
132 | setItem(JSON.stringify(history(searchHistory)));
133 | } else {
134 | setItem(JSON.stringify(history));
135 | }
136 | };
137 | const searchHistory = JSON.parse(item) as string[];
138 |
139 | return [searchHistory, setSearchHistory] as const;
140 | };
141 |
--------------------------------------------------------------------------------
/apps/stories/shadcn-ui/dropdown-menu.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 | import {
3 | LuCloud,
4 | LuCreditCard,
5 | LuGithub,
6 | LuKeyboard,
7 | LuLifeBuoy,
8 | LuLogOut,
9 | LuMail,
10 | LuMessageSquare,
11 | LuPlus,
12 | LuPlusCircle,
13 | LuSettings,
14 | LuUser,
15 | LuUserPlus,
16 | LuUsers,
17 | } from "react-icons/lu";
18 |
19 | import { Button } from "@/components/ui/button";
20 | import {
21 | DropdownMenu,
22 | DropdownMenuContent,
23 | DropdownMenuGroup,
24 | DropdownMenuItem,
25 | DropdownMenuLabel,
26 | DropdownMenuPortal,
27 | DropdownMenuSeparator,
28 | DropdownMenuShortcut,
29 | DropdownMenuSub,
30 | DropdownMenuSubContent,
31 | DropdownMenuSubTrigger,
32 | DropdownMenuTrigger,
33 | } from "@/components/ui/dropdown-menu";
34 |
35 | export default {
36 | title: "Shadcn-ui/DropdownMenu",
37 | component: DropdownMenu,
38 | parameters: {
39 | controls: { hideNoControlsWarning: true },
40 | },
41 | } as Meta;
42 | type Story = StoryObj;
43 |
44 | export const Default: Story = {
45 | render: () => (
46 |
47 | Open
48 |
49 | My Account
50 |
51 | Profile
52 | Billing
53 | Team
54 | Subscription
55 |
56 |
57 | ),
58 | };
59 |
60 | export const WithIcons: Story = {
61 | render: () => (
62 |
63 |
64 |
65 |
66 |
67 | My Account
68 |
69 |
70 |
71 |
72 | Profile
73 | ⇧⌘P
74 |
75 |
76 |
77 | Billing
78 | ⌘B
79 |
80 |
81 |
82 | Settings
83 | ⌘S
84 |
85 |
86 |
87 | Keyboard shortcuts
88 | ⌘K
89 |
90 |
91 |
92 |
93 |
94 |
95 | Team
96 |
97 |
98 |
99 |
100 | Invite users
101 |
102 |
103 |
104 |
105 |
106 | Email
107 |
108 |
109 |
110 | Message
111 |
112 |
113 |
114 |
115 | More...
116 |
117 |
118 |
119 |
120 |
121 |
122 | New Team
123 | ⌘+T
124 |
125 |
126 |
127 |
128 |
129 | GitHub
130 |
131 |
132 |
133 | Support
134 |
135 |
136 |
137 | API
138 |
139 |
140 |
141 |
142 | Log out
143 | ⇧⌘Q
144 |
145 |
146 |
147 | ),
148 | };
149 |
--------------------------------------------------------------------------------
/apps/web/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ToastPrimitives from "@radix-ui/react-toast";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { LuX } from "react-icons/lu";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ToastProvider = ToastPrimitives.Provider;
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24 |
25 | const toastVariants = cva(
26 | "data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "bg-background border",
31 | destructive:
32 | "group destructive border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | );
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | );
53 | });
54 | Toast.displayName = ToastPrimitives.Root.displayName;
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ));
69 | ToastAction.displayName = ToastPrimitives.Action.displayName;
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ));
87 | ToastClose.displayName = ToastPrimitives.Close.displayName;
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ));
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef;
114 |
115 | type ToastActionElement = React.ReactElement;
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | };
128 |
--------------------------------------------------------------------------------
/apps/web/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { DialogProps } from "@radix-ui/react-dialog";
5 | import { Command as CommandPrimitive } from "cmdk";
6 | import { LuSearch } from "react-icons/lu";
7 |
8 | import { cn } from "@/lib/utils";
9 | import { Dialog, DialogContent } from "@/components/ui/dialog";
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ));
24 | Command.displayName = CommandPrimitive.displayName;
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | );
38 | };
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ));
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName;
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ));
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName;
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ));
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ));
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName;
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ));
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName;
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | );
142 | };
143 | CommandShortcut.displayName = "CommandShortcut";
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | };
156 |
--------------------------------------------------------------------------------
/packages/tmdb-api/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # tmdb-api
2 |
3 | ## 0.1.8
4 |
5 | ### Patch Changes
6 |
7 | - [#47](https://github.com/mogeko/movisea/pull/47) [`9803a6e`](https://github.com/mogeko/movisea/commit/9803a6e53cc802d2bb0ca43ffd3ec657eabeca8d) Thanks [@mogeko](https://github.com/mogeko)! - Parse new requst parameter `body`
8 |
9 | - Updated dependencies [[`02c2203`](https://github.com/mogeko/movisea/commit/02c22031e9f0fd9d286abfc51c6f125eb6914090), [`14fc3a4`](https://github.com/mogeko/movisea/commit/14fc3a4829d4602b5935ced24ea13c3e451f4c04), [`9803a6e`](https://github.com/mogeko/movisea/commit/9803a6e53cc802d2bb0ca43ffd3ec657eabeca8d)]:
10 | - @mogeko/tmdb-request@1.3.2
11 |
12 | ## 0.1.7
13 |
14 | ### Patch Changes
15 |
16 | - [#45](https://github.com/mogeko/movisea/pull/45) [`aafbc9f`](https://github.com/mogeko/movisea/commit/aafbc9f8ea85254fb7e69f303adaf9d3a7e0cc1c) Thanks [@mogeko](https://github.com/mogeko)! - Modify type signatures to adapt to more stringent `tmdb-request` input parameters
17 |
18 | - Updated dependencies [[`772e193`](https://github.com/mogeko/movisea/commit/772e193647ab4f1aeb405e6be250de1b197914cf), [`040bc76`](https://github.com/mogeko/movisea/commit/040bc768e8934c4e0bbb955c3e12340b2da41784)]:
19 | - @mogeko/tmdb-request@1.3.1
20 |
21 | ## 0.1.6
22 |
23 | ### Patch Changes
24 |
25 | - Updated dependencies [[`f1edd70`](https://github.com/mogeko/movisea/commit/f1edd705fa368d3d5dbc5f4cfbbff93c4c972abd), [`67fad93`](https://github.com/mogeko/movisea/commit/67fad93720aa6d4716eaa5a312c11823152dec78), [`0c0916b`](https://github.com/mogeko/movisea/commit/0c0916bce1db46eee04f0dc5802fcb280294beb5), [`aa1fa17`](https://github.com/mogeko/movisea/commit/aa1fa176ffb4a14513b9b6dd1809e89f0b4d81b6)]:
26 | - @mogeko/tmdb-request@1.3.0
27 |
28 | ## 0.1.5
29 |
30 | ### Patch Changes
31 |
32 | - [#41](https://github.com/mogeko/movisea/pull/41) [`45b3dc9`](https://github.com/mogeko/movisea/commit/45b3dc97d6067f26f38e43ebdef21170fc482f59) Thanks [@mogeko](https://github.com/mogeko)! - Add types for account
33 |
34 | - [#41](https://github.com/mogeko/movisea/pull/41) [`0d6e608`](https://github.com/mogeko/movisea/commit/0d6e608d1b855c774c62e78c8a67a677f2b4d8ea) Thanks [@mogeko](https://github.com/mogeko)! - Add full endpoints about account
35 |
36 | - [#41](https://github.com/mogeko/movisea/pull/41) [`8742e1f`](https://github.com/mogeko/movisea/commit/8742e1fd979d9726b677c06f62f67371b8bfee97) Thanks [@mogeko](https://github.com/mogeko)! - Refactoring type system, and reduce the degree of coupling.
37 |
38 | - [#41](https://github.com/mogeko/movisea/pull/41) [`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774) Thanks [@mogeko](https://github.com/mogeko)! - Upgrade dependencies
39 |
40 | - bump `@types/node` from `20.2.5` to `20.3.3` ([#37](https://github.com/mogeko/movisea/pull/37))
41 |
42 | - Updated dependencies [[`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774)]:
43 | - @mogeko/tmdb-request@1.2.5
44 |
45 | ## 0.1.4
46 |
47 | ### Patch Changes
48 |
49 | - Updated dependencies [[`8a7c476`](https://github.com/mogeko/movisea/commit/8a7c4767fc817e495792e1ce99fbc12e6f4722b5)]:
50 | - @mogeko/tmdb-request@1.2.4
51 |
52 | ## 0.1.3
53 |
54 | ### Patch Changes
55 |
56 | - [#31](https://github.com/mogeko/movisea/pull/31) [`cc1aeaa`](https://github.com/mogeko/movisea/commit/cc1aeaa27fae98a1c8dea356e872cd72c0391da9) Thanks [@mogeko](https://github.com/mogeko)! - Rename `tmdbs-api-client` to `@mogeko/tmdb-api`
57 |
58 | ## 0.1.2
59 |
60 | ### Patch Changes
61 |
62 | - Updated dependencies [[`f02efa6`](https://github.com/mogeko/movisea/commit/f02efa69403ef02284b49ff0e0e7b050a9b4c99c)]:
63 | - tmdb-request@1.2.3
64 |
65 | ## 0.1.1
66 |
67 | ### Patch Changes
68 |
69 | - [`6e32e43`](https://github.com/mogeko/movisea/commit/6e32e43e32b6ab6b760f9190e57c0cb50964f402) Thanks [@mogeko](https://github.com/mogeko)! - Add Types for `SearchTVResult`.
70 |
71 | - [`78be03e`](https://github.com/mogeko/movisea/commit/78be03ee73a74d5d6963f03a076c6c7bc2da8ef8) Thanks [@mogeko](https://github.com/mogeko)! - Add type for SearchMovieResult
72 |
73 | ## 0.1.0
74 |
75 | ### Minor Changes
76 |
77 | - [#22](https://github.com/mogeko/movisea/pull/22) [`18c4ee4`](https://github.com/mogeko/movisea/commit/18c4ee46a40b2c0e739109f6d14b4709976401b2) Thanks [@mogeko](https://github.com/mogeko)! - Implement queries for each `Endpoints`
78 |
79 | ### Patch Changes
80 |
81 | - [#22](https://github.com/mogeko/movisea/pull/22) [`70d4402`](https://github.com/mogeko/movisea/commit/70d4402b3cbfffe37f6fc72794858f0270810c42) Thanks [@mogeko](https://github.com/mogeko)! - Create a main class and implement the wrapping of parser and request
82 |
83 | - Updated dependencies [[`a87825e`](https://github.com/mogeko/movisea/commit/a87825e9ee8de8e817d21ac09c6b23612c07c48c), [`a87825e`](https://github.com/mogeko/movisea/commit/a87825e9ee8de8e817d21ac09c6b23612c07c48c)]:
84 | - tmdb-request@1.2.2
85 |
86 | ## 0.0.6
87 |
88 | ### Patch Changes
89 |
90 | - Updated dependencies [[`0bfb3b1`](https://github.com/mogeko/movisea/commit/0bfb3b19ee76fcc89d33d9e200be815e50f60848), [`18c06db`](https://github.com/mogeko/movisea/commit/18c06db12b40056c4f287046e89a2117b704f6e8), [`3d13fcc`](https://github.com/mogeko/movisea/commit/3d13fcc1b9456b45aba9026fc7621caae711182d)]:
91 | - tmdb-request@1.2.1
92 |
93 | ## 0.0.5
94 |
95 | ### Patch Changes
96 |
97 | - Updated dependencies [[`3698e8d`](https://github.com/mogeko/movisea/commit/3698e8dfcb77f465519b84287ca95c464106d048)]:
98 | - tmdb-request@1.2.0
99 |
100 | ## 0.0.4
101 |
102 | ### Patch Changes
103 |
104 | - Updated dependencies [[`9393959`](https://github.com/mogeko/movisea/commit/9393959f8e7fcba6fc3c9d5d23713655863d9bbd)]:
105 | - tmdb-request@1.1.1
106 |
107 | ## 0.0.3
108 |
109 | ### Patch Changes
110 |
111 | - Updated dependencies [[`73e6e90`](https://github.com/mogeko/movisea/commit/73e6e9075ee8bd28bf10bfbd255cf7d43c56e0ca), [`2837897`](https://github.com/mogeko/movisea/commit/2837897af7d5c3b3396601ec1534f7ee86333215), [`f9ec3ad`](https://github.com/mogeko/movisea/commit/f9ec3adb187a7642a85db9a28c4ffe0284bbd7d6)]:
112 | - tmdb-request@1.1.0
113 |
114 | ## 0.0.2
115 |
116 | ### Patch Changes
117 |
118 | - Updated dependencies []:
119 | - tmdb-request@1.0.0
120 |
--------------------------------------------------------------------------------
/packages/tmdb-request/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # tmdb-request
2 |
3 | ## 1.3.2
4 |
5 | ### Patch Changes
6 |
7 | - [#47](https://github.com/mogeko/movisea/pull/47) [`02c2203`](https://github.com/mogeko/movisea/commit/02c22031e9f0fd9d286abfc51c6f125eb6914090) Thanks [@mogeko](https://github.com/mogeko)! - Keep `body` alway be `null` unless method is `POST`
8 |
9 | - [#47](https://github.com/mogeko/movisea/pull/47) [`14fc3a4`](https://github.com/mogeko/movisea/commit/14fc3a4829d4602b5935ced24ea13c3e451f4c04) Thanks [@mogeko](https://github.com/mogeko)! - Add a guide to load `@mogeko/tmdb-request` directly in the browser (from [esm.sh](https://esm.sh)).
10 |
11 | - [#47](https://github.com/mogeko/movisea/pull/47) [`9803a6e`](https://github.com/mogeko/movisea/commit/9803a6e53cc802d2bb0ca43ffd3ec657eabeca8d) Thanks [@mogeko](https://github.com/mogeko)! - Parse new requst parameter `body`
12 |
13 | ## 1.3.1
14 |
15 | ### Patch Changes
16 |
17 | - [#45](https://github.com/mogeko/movisea/pull/45) [`772e193`](https://github.com/mogeko/movisea/commit/772e193647ab4f1aeb405e6be250de1b197914cf) Thanks [@mogeko](https://github.com/mogeko)! - Add TSDoc for each known request parameter
18 |
19 | - [#45](https://github.com/mogeko/movisea/pull/45) [`040bc76`](https://github.com/mogeko/movisea/commit/040bc768e8934c4e0bbb955c3e12340b2da41784) Thanks [@mogeko](https://github.com/mogeko)! - Standardize the input of parameters by [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html).
20 |
21 | ## 1.3.0
22 |
23 | ### Minor Changes
24 |
25 | - [#43](https://github.com/mogeko/movisea/pull/43) [`f1edd70`](https://github.com/mogeko/movisea/commit/f1edd705fa368d3d5dbc5f4cfbbff93c4c972abd) Thanks [@mogeko](https://github.com/mogeko)! - Compatible with `:varname` syntax for url template
26 |
27 | ### Patch Changes
28 |
29 | - [#43](https://github.com/mogeko/movisea/pull/43) [`67fad93`](https://github.com/mogeko/movisea/commit/67fad93720aa6d4716eaa5a312c11823152dec78) Thanks [@mogeko](https://github.com/mogeko)! - Extend `splitObj` function by paradigm
30 |
31 | - [#43](https://github.com/mogeko/movisea/pull/43) [`0c0916b`](https://github.com/mogeko/movisea/commit/0c0916bce1db46eee04f0dc5802fcb280294beb5) Thanks [@mogeko](https://github.com/mogeko)! - Adjust the type system
32 |
33 | - [#43](https://github.com/mogeko/movisea/pull/43) [`aa1fa17`](https://github.com/mogeko/movisea/commit/aa1fa176ffb4a14513b9b6dd1809e89f0b4d81b6) Thanks [@mogeko](https://github.com/mogeko)! - Write TSDoc for all exported interfaces and types
34 |
35 | ## 1.2.5
36 |
37 | ### Patch Changes
38 |
39 | - [#41](https://github.com/mogeko/movisea/pull/41) [`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774) Thanks [@mogeko](https://github.com/mogeko)! - Upgrade dependencies
40 |
41 | - bump `@types/node` from `20.2.5` to `20.3.3` ([#37](https://github.com/mogeko/movisea/pull/37))
42 |
43 | - Updated dependencies [[`c7304c1`](https://github.com/mogeko/movisea/commit/c7304c10629a443c00465c41e1d32ca1c4de9774)]:
44 | - @mogeko/is-plain-object@0.0.3
45 |
46 | ## 1.2.4
47 |
48 | ### Patch Changes
49 |
50 | - [#33](https://github.com/mogeko/movisea/pull/33) [`8a7c476`](https://github.com/mogeko/movisea/commit/8a7c4767fc817e495792e1ce99fbc12e6f4722b5) Thanks [@mogeko](https://github.com/mogeko)! - Rename `tmdb-request` to `@mogeko/tmdb-request`
51 |
52 | ## 1.2.3
53 |
54 | ### Patch Changes
55 |
56 | - [#28](https://github.com/mogeko/movisea/pull/28) [`f02efa6`](https://github.com/mogeko/movisea/commit/f02efa69403ef02284b49ff0e0e7b050a9b4c99c) Thanks [@mogeko](https://github.com/mogeko)! - Replace `is-plain-object` by our own implemented
57 |
58 | - Updated dependencies [[`f02efa6`](https://github.com/mogeko/movisea/commit/f02efa69403ef02284b49ff0e0e7b050a9b4c99c)]:
59 | - @mogeko/is-plain-object@0.0.2
60 |
61 | ## 1.2.2
62 |
63 | ### Patch Changes
64 |
65 | - [#22](https://github.com/mogeko/movisea/pull/22) [`a87825e`](https://github.com/mogeko/movisea/commit/a87825e9ee8de8e817d21ac09c6b23612c07c48c) Thanks [@mogeko](https://github.com/mogeko)! - Re-export `mergeDeep` function as `./merge-deep`
66 |
67 | - [#22](https://github.com/mogeko/movisea/pull/22) [`a87825e`](https://github.com/mogeko/movisea/commit/a87825e9ee8de8e817d21ac09c6b23612c07c48c) Thanks [@mogeko](https://github.com/mogeko)! - Re-export `Options` type for `request` and `parser` functions
68 |
69 | ## 1.2.1
70 |
71 | ### Patch Changes
72 |
73 | - [#18](https://github.com/mogeko/movisea/pull/18) [`0bfb3b1`](https://github.com/mogeko/movisea/commit/0bfb3b19ee76fcc89d33d9e200be815e50f60848) Thanks [@mogeko](https://github.com/mogeko)! - Add TSDoc/JSDoc for `request` function
74 |
75 | - [#18](https://github.com/mogeko/movisea/pull/18) [`18c06db`](https://github.com/mogeko/movisea/commit/18c06db12b40056c4f287046e89a2117b704f6e8) Thanks [@mogeko](https://github.com/mogeko)! - Add TSDoc/JSDoc for `parser` function
76 |
77 | - [#18](https://github.com/mogeko/movisea/pull/18) [`3d13fcc`](https://github.com/mogeko/movisea/commit/3d13fcc1b9456b45aba9026fc7621caae711182d) Thanks [@mogeko](https://github.com/mogeko)! - Supplement the TSDoc/JSDoc for enter functions
78 |
79 | ## 1.2.0
80 |
81 | ### Minor Changes
82 |
83 | - [#16](https://github.com/mogeko/movisea/pull/16) [`3698e8d`](https://github.com/mogeko/movisea/commit/3698e8dfcb77f465519b84287ca95c464106d048) Thanks [@mogeko](https://github.com/mogeko)! - The parsing results will distinguish between `baseUrl` and `url`.
84 |
85 | ## 1.1.1
86 |
87 | ### Patch Changes
88 |
89 | - [#13](https://github.com/mogeko/movisea/pull/13) [`9393959`](https://github.com/mogeko/movisea/commit/9393959f8e7fcba6fc3c9d5d23713655863d9bbd) Thanks [@mogeko](https://github.com/mogeko)! - Separate out fetch's wraper fetcher
90 |
91 | ## 1.1.0
92 |
93 | ### Minor Changes
94 |
95 | - [`73e6e90`](https://github.com/mogeko/movisea/commit/73e6e9075ee8bd28bf10bfbd255cf7d43c56e0ca) Thanks [@mogeko](https://github.com/mogeko)! - add additional meaning to parser
96 |
97 | Nowadays, We allow `route` to be empty. In this case, we get the `route` (`url` and `method`) from `opts`.
98 |
99 | - [#9](https://github.com/mogeko/movisea/pull/9) [`2837897`](https://github.com/mogeko/movisea/commit/2837897af7d5c3b3396601ec1534f7ee86333215) Thanks [@mogeko](https://github.com/mogeko)! - Adjust the types so that the type system model can better describe the actual situation.
100 |
101 | - [#9](https://github.com/mogeko/movisea/pull/9) [`f9ec3ad`](https://github.com/mogeko/movisea/commit/f9ec3adb187a7642a85db9a28c4ffe0284bbd7d6) Thanks [@mogeko](https://github.com/mogeko)! - Modify the overload of the `request` function to keep identical behavior.
102 |
103 | ## 1.0.0
104 |
105 | ### Major Changes
106 |
107 | - This is the minimum available version of this library.
108 |
--------------------------------------------------------------------------------
/apps/web/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5 | import { LuCheck, LuChevronRight, LuCircle } from "react-icons/lu";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root;
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean;
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ));
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName;
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ));
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName;
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ));
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean;
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ));
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ));
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName;
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ));
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean;
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ));
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ));
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | );
181 | };
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | };
201 |
--------------------------------------------------------------------------------