From 2572c4373320dc879665ab947b6a1ea0fef0ffdf Mon Sep 17 00:00:00 2001 From: StepanovPlaton Date: Sat, 18 May 2024 20:30:15 +0400 Subject: [PATCH] Add login --- package-lock.json | 70 +++++++++++ package.json | 6 + src/app/@auth/(.)login/page.tsx | 73 +++++++++++- src/app/@auth/[...catchAll]/page.tsx | 3 - src/app/globals.css | 6 +- src/app/layout.tsx | 36 +++--- src/app/login/page.tsx | 4 +- src/entities/game/game.ts | 2 +- src/entities/game/schemas/gameCard.ts | 50 ++++---- src/entities/user/index.ts | 16 +++ src/entities/user/schemas/auth.ts | 30 +++++ src/entities/user/schemas/user.ts | 8 ++ src/entities/user/user.ts | 48 ++++++++ src/features/userActivities/index.ts | 3 + .../userActivities/userActivities.tsx | 29 +++++ src/shared/http/httpService.ts | 34 ------ src/shared/ui/index.ts | 3 + src/shared/ui/modal.tsx | 33 ++++++ .../{get_youtube_id.ts => getYoutubeId.ts} | 0 src/shared/utils/http.ts | 53 +++++++++ src/shared/utils/index.ts | 2 +- src/widgets/header/header.tsx | 109 +++++++++--------- src/widgets/section/section.tsx | 14 ++- tailwind.config.ts | 79 ++++++++----- 24 files changed, 536 insertions(+), 175 deletions(-) delete mode 100644 src/app/@auth/[...catchAll]/page.tsx create mode 100644 src/entities/user/index.ts create mode 100644 src/entities/user/schemas/auth.ts create mode 100644 src/entities/user/schemas/user.ts create mode 100644 src/entities/user/user.ts create mode 100644 src/features/userActivities/index.ts create mode 100644 src/features/userActivities/userActivities.tsx delete mode 100644 src/shared/http/httpService.ts create mode 100644 src/shared/ui/index.ts create mode 100644 src/shared/ui/modal.tsx rename src/shared/utils/{get_youtube_id.ts => getYoutubeId.ts} (100%) create mode 100644 src/shared/utils/http.ts diff --git a/package-lock.json b/package-lock.json index c1d7269..7cea639 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,18 @@ "name": "torrent_frontend", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.4.0", + "@types/js-cookie": "^3.0.6", "clsx": "^2.1.1", + "js-cookie": "^3.0.5", + "jwt-decode": "^4.0.0", "next": "14.2.3", "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.4", "react-responsive-masonry": "^2.2.0", + "swr": "^2.2.5", "zod": "^3.23.8" }, "devDependencies": { @@ -108,6 +114,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.4.0.tgz", + "integrity": "sha512-+oAqK3okmoEDnvUkJ3N/mvNMeeMv5Apgy1jkoRmlaaAF4vBgcJs9tHvtXU7VE4DvPosvAUUkPOaNFunzt1dbgA==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -447,6 +461,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2880,6 +2899,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2942,6 +2969,14 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3749,6 +3784,21 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.51.4", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz", + "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -4346,6 +4396,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/tailwindcss": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", @@ -4606,6 +4668,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index d27bd09..baed7fb 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,18 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.4.0", + "@types/js-cookie": "^3.0.6", "clsx": "^2.1.1", + "js-cookie": "^3.0.5", + "jwt-decode": "^4.0.0", "next": "14.2.3", "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.4", "react-responsive-masonry": "^2.2.0", + "swr": "^2.2.5", "zod": "^3.23.8" }, "devDependencies": { diff --git a/src/app/@auth/(.)login/page.tsx b/src/app/@auth/(.)login/page.tsx index 88c0817..4e0a65c 100644 --- a/src/app/@auth/(.)login/page.tsx +++ b/src/app/@auth/(.)login/page.tsx @@ -1,3 +1,74 @@ +"use client"; + +import { + LoginForm, + loginFormFieldNames, + loginFormSchema, +} from "@/entities/user"; +import { UserService } from "@/entities/user/user"; +import { Modal } from "@/shared/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { mutate } from "swr"; + export default function Login() { - return <>123; + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ resolver: zodResolver(loginFormSchema) }); + + const router = useRouter(); + + const onSubmit: SubmitHandler = async (data) => { + const userInfo = await UserService.Login(data); + mutate("user", userInfo); + router.back(); + }; + + return ( + +
+
+

.Torrent

+ {(["username", "password"] as ("username" | "password")[]).map( + (field) => ( + + ) + )} + + +
+
+
+ ); } diff --git a/src/app/@auth/[...catchAll]/page.tsx b/src/app/@auth/[...catchAll]/page.tsx deleted file mode 100644 index 1fd97c2..0000000 --- a/src/app/@auth/[...catchAll]/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function CatchAll() { - return null; -} diff --git a/src/app/globals.css b/src/app/globals.css index 6f02f97..18e0ec9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -15,6 +15,8 @@ --color-ac1: #fabd2f; --color-ac2: #8ec07c; + --color-err: #cc241d; + --app-width: 70%; font-size: calc((100vw / 1920) * 20); } @@ -31,6 +33,8 @@ --color-ac0: #076678; --color-ac1: #b57614; --color-ac2: #427b58; + + --color-err: #cc241d; } html, @@ -45,7 +49,7 @@ body { transition-property: color, background-color, border-color; transition-duration: 0.3s; - /* overflow: hidden; */ + overflow: hidden; color: var(--color-fg1); background-color: var(--color-bg0); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f104e48..00e87b4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,24 +6,24 @@ import { Header } from "@/widgets/header"; const inter = Inter({ subsets: ["latin"] }); export default function RootLayout({ - auth, - children, + auth, + children, }: Readonly<{ - auth: React.ReactNode; - children: React.ReactNode; + auth: React.ReactNode; + children: React.ReactNode; }>) { - return ( - // suppressHydrationWarning for theme support - - - - {auth} -
-
- {children} -
- - - - ); + return ( + // suppressHydrationWarning for theme support + + + + {auth} +
+
+ {children} +
+ + + + ); } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 0a66c4f..7d37d98 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,3 +1,5 @@ +import { redirect } from "next/navigation"; + export default function Login() { - return <>login page; + redirect("/"); } diff --git a/src/entities/game/game.ts b/src/entities/game/game.ts index 5478154..ad8c365 100644 --- a/src/entities/game/game.ts +++ b/src/entities/game/game.ts @@ -1,4 +1,4 @@ -import { HTTPService } from "@/shared/http/httpService"; +import { HTTPService } from "@/shared/utils/http"; import { gameCardsSchema, GameCardType } from "./schemas/gameCard"; import { gameSchema, GameType } from "./schemas/game"; diff --git a/src/entities/game/schemas/gameCard.ts b/src/entities/game/schemas/gameCard.ts index a85bc35..1654958 100644 --- a/src/entities/game/schemas/gameCard.ts +++ b/src/entities/game/schemas/gameCard.ts @@ -1,35 +1,35 @@ import { z } from "zod"; export const gameCardSchema = z - .object({ - id: z.number(), - title: z.string().min(3), - cover: z.string().optional(), - description: z.string().optional(), - version: z.string().optional(), - }) - .transform((card) => { - return { - ...card, - cover: card.cover - ? process.env.NEXT_PUBLIC_COVER_FULL_URL + "/" + card.cover - : undefined, - cover_preview: card.cover - ? process.env.NEXT_PUBLIC_COVER_PREVIEW_URL + "/" + card.cover - : undefined, - }; - }); + .object({ + id: z.number().positive(), + title: z.string().min(3), + cover: z.string().optional(), + description: z.string().optional(), + version: z.string().optional(), + }) + .transform((card) => { + return { + ...card, + cover: card.cover + ? process.env.NEXT_PUBLIC_COVER_FULL_URL + "/" + card.cover + : undefined, + cover_preview: card.cover + ? process.env.NEXT_PUBLIC_COVER_PREVIEW_URL + "/" + card.cover + : undefined, + }; + }); export type GameCardType = z.infer; export const isGameCard = (a: any): a is GameCardType => { - return gameCardSchema.safeParse(a).success; + return gameCardSchema.safeParse(a).success; }; export const gameCardsSchema = z.array(z.any()).transform((a) => { - const cards: GameCardType[] = []; - a.forEach((e) => { - if (isGameCard(e)) cards.push(gameCardSchema.parse(e)); - else console.error("GameCard parse error - ", e); - }); - return cards; + const cards: GameCardType[] = []; + a.forEach((e) => { + if (isGameCard(e)) cards.push(gameCardSchema.parse(e)); + else console.error("GameCard parse error - ", e); + }); + return cards; }); diff --git a/src/entities/user/index.ts b/src/entities/user/index.ts new file mode 100644 index 0000000..f4dfbc5 --- /dev/null +++ b/src/entities/user/index.ts @@ -0,0 +1,16 @@ +import { + loginFormSchema, + loginFormFieldNames, + LoginForm, +} from "./schemas/auth"; +import { userSchema, User } from "./schemas/user"; +import { UserService } from "./user"; + +export { + loginFormSchema, + loginFormFieldNames, + UserService, + userSchema, + type User, + type LoginForm, +}; diff --git a/src/entities/user/schemas/auth.ts b/src/entities/user/schemas/auth.ts new file mode 100644 index 0000000..1a14f96 --- /dev/null +++ b/src/entities/user/schemas/auth.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; +import { userSchema } from "./user"; + +export const loginFormSchema = z.object({ + username: z.string().min(3, "Логин слишком короткий"), + password: z.string().min(3, "Пароль слишком короткий"), +}); +export const loginFormFieldNames = { + username: "Логин", + password: "Пароль", +}; +export type LoginForm = z.infer; + +export const tokenResponseSchema = z + .object({ + access_token: z.string(), + token_type: z.string(), + }) + .transform((tokenResponse) => tokenResponse.access_token); +export type TokenResponse = z.infer; + +export const tokenDataSchema = userSchema.merge( + z.object({ + expire: z + .string() + .min(1) + .transform((d) => new Date(d)), + }) +); +export type TokenData = z.infer; diff --git a/src/entities/user/schemas/user.ts b/src/entities/user/schemas/user.ts new file mode 100644 index 0000000..ce284e6 --- /dev/null +++ b/src/entities/user/schemas/user.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const userSchema = z.object({ + id: z.number().positive(), + name: z.string().min(3), + email: z.string().min(3), +}); +export type User = z.infer; diff --git a/src/entities/user/user.ts b/src/entities/user/user.ts new file mode 100644 index 0000000..e11490b --- /dev/null +++ b/src/entities/user/user.ts @@ -0,0 +1,48 @@ +import { HTTPService } from "@/shared/utils/http"; +import { + LoginForm, + TokenData, + tokenDataSchema, + TokenResponse, + tokenResponseSchema, +} from "./schemas/auth"; +import { jwtDecode } from "jwt-decode"; +import Cookies from "js-cookie"; + +export abstract class UserService { + public static async Login(loginForm: LoginForm) { + const accessToken = await HTTPService.post( + "/auth", + new URLSearchParams(Object.entries(loginForm)), + tokenResponseSchema, + { + "Content-Type": "application/x-www-form-urlencoded", + } + ); + if (accessToken) { + const tokenData = this.DecodeToken(accessToken); + if (tokenData) { + Cookies.set("access-token", accessToken, { + secure: true, + expires: tokenData.expire, + }); + return tokenData; + } + } + } + + public static IdentifyYourself(): TokenData | undefined { + const token = Cookies.get("access-token"); + if (token) { + return this.DecodeToken(token); + } + } + + public static DecodeToken(token: string): TokenData | undefined { + const tokenPayload = jwtDecode(token); + const parseResult = tokenDataSchema.safeParse(tokenPayload); + if (parseResult.success) { + return parseResult.data; + } else console.error("JWT payload broken - " + parseResult.error); + } +} diff --git a/src/features/userActivities/index.ts b/src/features/userActivities/index.ts new file mode 100644 index 0000000..e3733c7 --- /dev/null +++ b/src/features/userActivities/index.ts @@ -0,0 +1,3 @@ +import { UserActivities } from "./userActivities"; + +export { UserActivities }; diff --git a/src/features/userActivities/userActivities.tsx b/src/features/userActivities/userActivities.tsx new file mode 100644 index 0000000..e7364f1 --- /dev/null +++ b/src/features/userActivities/userActivities.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { UserService } from "@/entities/user"; +import { PersonIcon } from "@/shared/assets/icons"; +import Link from "next/link"; +import useSWR from "swr"; + +export const UserActivities = () => { + const { data: me } = useSWR("user", () => UserService.IdentifyYourself()); + + return ( + <> + + {me && ( + + {me.name} + + )} + {!me && ( + + Войти + + )} + + ); +}; diff --git a/src/shared/http/httpService.ts b/src/shared/http/httpService.ts deleted file mode 100644 index e0b8888..0000000 --- a/src/shared/http/httpService.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { z } from "zod"; - -export abstract class HTTPService { - public static async get( - url: string, - schema: z.ZodTypeAny - ): Promise { - return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, { - method: "GET", - headers: { - accept: "application/json", - }, - cache: "no-cache", - }) - .then((r) => { - if (r && r.ok) return r; - else throw Error("Response ok = false"); - }) - .then((r) => r.json()) - .then((d) => { - const parseResult = schema.safeParse(d); - if (parseResult.success) { - return parseResult.data as Z; - } else { - console.error(parseResult.error); - return null; - } - }) - .catch((e) => { - console.error(e); - return null; - }); - } -} diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts new file mode 100644 index 0000000..f8d5740 --- /dev/null +++ b/src/shared/ui/index.ts @@ -0,0 +1,3 @@ +import { Modal } from "./modal"; + +export { Modal }; diff --git a/src/shared/ui/modal.tsx b/src/shared/ui/modal.tsx new file mode 100644 index 0000000..3630887 --- /dev/null +++ b/src/shared/ui/modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import clsx from "clsx"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export const Modal = ({ children }: { children: React.ReactNode }) => { + const [closing, setClosing] = useState(false); + const router = useRouter(); + return ( +
{ + setClosing(true); + setTimeout(() => router.back(), 500); + }} + > +
{ + e.stopPropagation(); + }} + > + {children} +
+
+ ); +}; diff --git a/src/shared/utils/get_youtube_id.ts b/src/shared/utils/getYoutubeId.ts similarity index 100% rename from src/shared/utils/get_youtube_id.ts rename to src/shared/utils/getYoutubeId.ts diff --git a/src/shared/utils/http.ts b/src/shared/utils/http.ts new file mode 100644 index 0000000..ff26c18 --- /dev/null +++ b/src/shared/utils/http.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +export abstract class HTTPService { + public static async get( + url: string, + schema: z.ZodTypeAny + ): Promise { + return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, { + method: "GET", + headers: { + accept: "application/json", + }, + cache: "no-cache", + }) + .then((r) => { + if (r && r.ok) return r; + else throw Error("Response ok = false"); + }) + .then((r) => r.json()) + .then((d) => schema.parse(d)) + .catch((e) => { + console.error(e); + return null; + }); + } + + public static async post( + url: string, + body: BodyInit, + schema: z.ZodTypeAny, + headers?: HeadersInit + ): Promise { + return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, { + method: "POST", + headers: { + accept: "application/json", + ...headers, + }, + body: body, + cache: "no-cache", + }) + .then((r) => { + if (r && r.ok) return r; + else throw Error("Response ok = false"); + }) + .then((r) => r.json()) + .then((d) => schema.parse(d)) + .catch((e) => { + console.error(e); + return null; + }); + } +} diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index 6ff29e9..c37acc9 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -1,3 +1,3 @@ -import { getYouTubeID } from "./get_youtube_id"; +import { getYouTubeID } from "./getYoutubeId"; export { getYouTubeID }; diff --git a/src/widgets/header/header.tsx b/src/widgets/header/header.tsx index d25772c..f3b58e5 100644 --- a/src/widgets/header/header.tsx +++ b/src/widgets/header/header.tsx @@ -1,74 +1,69 @@ "use client"; import { SchemeSwitch } from "@/features/colorSchemeSwitch"; -import { PersonIcon, SearchIcon } from "@/shared/assets/icons"; +import { SearchIcon } from "@/shared/assets/icons"; import { MobileMenu } from "./mobileMenu/mobileMenu"; import Link from "next/link"; import { useSelectedLayoutSegment } from "next/navigation"; import clsx from "clsx"; +import { UserActivities } from "@/features/userActivities"; const sections = [ - { title: "Игры", href: "games" }, - { title: "Фильмы", href: "films" }, - { title: "Аудиокниги", href: "audiobooks" }, + { title: "Игры", href: "games" }, + { title: "Фильмы", href: "films" }, + { title: "Аудиокниги", href: "audiobooks" }, ]; export const Header = () => { - const currentPageName = useSelectedLayoutSegment(); + const currentPageName = useSelectedLayoutSegment(); - return ( -
-
+
-

-
- -
- .Torrent -

-
- {sections.map((section) => ( - - {section.title} - - ))} -
-
- - - - - Войти - - -
-
- ); + > + + Поиск + + + + +
+ ); }; diff --git a/src/widgets/section/section.tsx b/src/widgets/section/section.tsx index e138174..95e1636 100644 --- a/src/widgets/section/section.tsx +++ b/src/widgets/section/section.tsx @@ -1,8 +1,11 @@ "use client"; +import clsx from "clsx"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; import Masonry, { ResponsiveMasonry } from "react-responsive-masonry"; +import { boolean } from "zod"; export const Section = ({ name, @@ -16,6 +19,9 @@ export const Section = ({ children: React.ReactNode; }) => { const router = useRouter(); + const [loaded, setLoaded] = useState(false); + + useEffect(() => setLoaded(true), []); return (
@@ -27,7 +33,13 @@ export const Section = ({ {name} )} - + {children} {link && invite_text && ( diff --git a/tailwind.config.ts b/tailwind.config.ts index 14d5e41..06c7c00 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,37 +1,52 @@ import type { Config } from "tailwindcss"; const config: Config = { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - "./src/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - colors: { - bg0: "var(--color-bg0)", - bg1: "var(--color-bg1)", - bg4: "var(--color-bg4)", - fg0: "var(--color-fg0)", - fg1: "var(--color-fg1)", - fg4: "var(--color-fg4)", - ac0: "var(--color-ac0)", - ac1: "var(--color-ac1)", - ac2: "var(--color-ac2)", - }, - }, - screens: { - tb: "640px", - lp: "1024px", - dsk: "1280px", - }, - }, - plugins: [], + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + "./src/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + colors: { + bg0: "var(--color-bg0)", + bg1: "var(--color-bg1)", + bg4: "var(--color-bg4)", + fg0: "var(--color-fg0)", + fg1: "var(--color-fg1)", + fg4: "var(--color-fg4)", + ac0: "var(--color-ac0)", + ac1: "var(--color-ac1)", + ac2: "var(--color-ac2)", + err: "var(--color-err)", + }, + animation: { + fadeIn: "fadeIn 0.25s ease-in-out", + fadeOut: "fadeOut 0.25s ease-in-out", + }, + keyframes: () => ({ + fadeIn: { + "0%": { opacity: "0" }, + "100%": { opacity: "1" }, + }, + fadeOut: { + "0%": { opacity: "1" }, + "100%": { opacity: "0" }, + }, + }), + }, + screens: { + tb: "640px", + lp: "1024px", + dsk: "1280px", + }, + }, + plugins: [], }; export default config;