diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 311d660..552affd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,28 +1,28 @@ -name: Bun CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build-testing: - runs-on: ubuntu-22.04 - container: - image: oven/bun:latest - steps: - - name: Install Git - run: apt update && apt install -y git - - - name: Clone private repo - run: git clone "$GITEA_REPOSITORY_CLONE_URL" . - - - name: Install dependencies - run: bun install - - - name: Running lint - run: bun run lint - - - name: Running build - run: bun run build +name: Bun CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-testing: + runs-on: ubuntu-22.04 + container: + image: oven/bun:latest + steps: + - name: Install Git + run: apt update && apt install -y git + + - name: Clone private repo + run: git clone "$GITEA_REPOSITORY_CLONE_URL" . + + - name: Install dependencies + run: bun install + + - name: Running lint + run: bun run lint + + - name: Running build + run: bun run build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7e7ee2..dbe81b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,31 +1,31 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Install Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install dependencies - run: bun install - - - name: Run lint - run: bun run lint - - - name: Run build +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run lint + run: bun run lint + + - name: Run build run: bun run build \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ba32ab2..56fe8b3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,18 +1,18 @@ -image: oven/bun:latest - -stages: - - lint - - build - -before_script: - - bun install - -lint: - stage: lint - script: - - bun run lint - -build: - stage: build - script: - - bun run build +image: oven/bun:latest + +stages: + - lint + - build + +before_script: + - bun install + +lint: + stage: lint + script: + - bun run lint + +build: + stage: build + script: + - bun run build diff --git a/app/(auth)/auth/callback/[...provider]/page.tsx b/app/(auth)/auth/callback/[...provider]/page.tsx index faca01d..6ade1eb 100644 --- a/app/(auth)/auth/callback/[...provider]/page.tsx +++ b/app/(auth)/auth/callback/[...provider]/page.tsx @@ -1,8 +1,8 @@ -import OauthCallbackHandler from "@/features/oauth-callback/pages/callbackHandler"; -import React from "react"; - -const page = () => { - return ; -}; - -export default page; +import OauthCallbackHandler from "@/features/oauth-callback/pages/callbackHandler"; +import React from "react"; + +const page = () => { + return ; +}; + +export default page; diff --git a/app/(auth)/login/metadata.tsx b/app/(auth)/login/metadata.tsx index 3ca9314..da139ce 100644 --- a/app/(auth)/login/metadata.tsx +++ b/app/(auth)/login/metadata.tsx @@ -1,5 +1,5 @@ -import { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Login | Nounoz TV", -}; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Login | Nounoz TV", +}; diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 71628a2..dd75146 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,11 +1,11 @@ -import LoginPage from "@/features/auth/pages/LoginPage"; -import { metadata } from "./metadata"; -export { metadata }; - -import React from "react"; - -const page = () => { - return ; -}; - -export default page; +import LoginPage from "@/features/auth/pages/LoginPage"; +import { metadata } from "./metadata"; +export { metadata }; + +import React from "react"; + +const page = () => { + return ; +}; + +export default page; diff --git a/app/(auth)/signup/metadata.tsx b/app/(auth)/signup/metadata.tsx index 77bb0d4..3971bf7 100644 --- a/app/(auth)/signup/metadata.tsx +++ b/app/(auth)/signup/metadata.tsx @@ -1,5 +1,5 @@ -import { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Sign Up | Nounoz TV", -}; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Sign Up | Nounoz TV", +}; diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index fb33e99..ca4dc82 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -1,11 +1,11 @@ -import SignupPage from "@/features/auth/pages/SignupPage"; -import { metadata } from "./metadata"; -export { metadata }; - -import React from "react"; - -const page = () => { - return ; -}; - -export default page; +import SignupPage from "@/features/auth/pages/SignupPage"; +import { metadata } from "./metadata"; +export { metadata }; + +import React from "react"; + +const page = () => { + return ; +}; + +export default page; diff --git a/app/(main)/(home)/button.tsx b/app/(main)/(home)/button.tsx index 1048ff9..f549df8 100644 --- a/app/(main)/(home)/button.tsx +++ b/app/(main)/(home)/button.tsx @@ -1,10 +1,10 @@ -"use client"; - -import { Button } from "@heroui/react"; -import React from "react"; - -const button = () => { - return ; -}; - -export default button; +"use client"; + +import { Button } from "@heroui/react"; +import React from "react"; + +const button = () => { + return ; +}; + +export default button; diff --git a/app/(main)/(home)/metadata.tsx b/app/(main)/(home)/metadata.tsx index 6a4230a..6469e7d 100644 --- a/app/(main)/(home)/metadata.tsx +++ b/app/(main)/(home)/metadata.tsx @@ -1,39 +1,39 @@ -import { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Nounoz TV - Anime Streaming Station Center", - description: - "Nounoz TV adalah tempat santai buat nonton anime kualitas tinggi tanpa ribet. Didukung komunitas yang aktif dan ramah, kamu nggak cuma nonton—tapi juga bisa ngobrol, sharing, dan seru-seruan bareng.", - keywords: [ - "nonton anime", - "streaming anime", - "anime sub indo", - "anime HD", - "komunitas anime", - "Nounoz TV", - ], - openGraph: { - title: "Nounoz TV - Streaming Anime HD + Komunitas Asik", - description: - "Nonton anime jadi lebih seru bareng teman-teman. Kualitas jernih, tanpa iklan ganggu, dan selalu update!", - url: "https://nounoz.tv", - siteName: "Nounoz TV", - images: [ - { - url: "https://nounoz.tv/og-image.jpg", - width: 1200, - height: 630, - alt: "Nounoz TV - Nonton Anime HD Bareng Komunitas", - }, - ], - locale: "id_ID", - type: "website", - }, - twitter: { - card: "summary_large_image", - title: "Nounoz TV - Nonton Anime HD Bareng Komunitas", - description: - "Streaming anime kualitas tinggi sambil ngobrol santai bareng komunitas yang aktif dan suportif.", - images: ["https://nounoz.tv/og-image.jpg"], - }, -}; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Nounoz TV - Anime Streaming Station Center", + description: + "Nounoz TV adalah tempat santai buat nonton anime kualitas tinggi tanpa ribet. Didukung komunitas yang aktif dan ramah, kamu nggak cuma nonton—tapi juga bisa ngobrol, sharing, dan seru-seruan bareng.", + keywords: [ + "nonton anime", + "streaming anime", + "anime sub indo", + "anime HD", + "komunitas anime", + "Nounoz TV", + ], + openGraph: { + title: "Nounoz TV - Streaming Anime HD + Komunitas Asik", + description: + "Nonton anime jadi lebih seru bareng teman-teman. Kualitas jernih, tanpa iklan ganggu, dan selalu update!", + url: "https://nounoz.tv", + siteName: "Nounoz TV", + images: [ + { + url: "https://nounoz.tv/og-image.jpg", + width: 1200, + height: 630, + alt: "Nounoz TV - Nonton Anime HD Bareng Komunitas", + }, + ], + locale: "id_ID", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Nounoz TV - Nonton Anime HD Bareng Komunitas", + description: + "Streaming anime kualitas tinggi sambil ngobrol santai bareng komunitas yang aktif dan suportif.", + images: ["https://nounoz.tv/og-image.jpg"], + }, +}; diff --git a/app/(main)/explore/page.tsx b/app/(main)/explore/page.tsx index 2939c5c..f1fae19 100644 --- a/app/(main)/explore/page.tsx +++ b/app/(main)/explore/page.tsx @@ -1,7 +1,7 @@ -import React from "react"; - -const page = () => { - return
Explore Page
; -}; - -export default page; +import React from "react"; + +const page = () => { + return
Explore Page
; +}; + +export default page; diff --git a/app/(main)/genres/page.tsx b/app/(main)/genres/page.tsx index 56d0140..f7900c9 100644 --- a/app/(main)/genres/page.tsx +++ b/app/(main)/genres/page.tsx @@ -1,7 +1,7 @@ -import React from "react"; - -const page = () => { - return
Genre Page
; -}; - -export default page; +import React from "react"; + +const page = () => { + return
Genre Page
; +}; + +export default page; diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 84daa17..2b680ca 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -1,13 +1,13 @@ -import NavbarUI from "@/widgets/navbar/ui/Navbar"; -import React from "react"; - -const mainLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { - return ( -
- -
{children}
-
- ); -}; - -export default mainLayout; +import NavbarUI from "@/widgets/navbar/ui/Navbar"; +import React from "react"; + +const mainLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { + return ( +
+ +
{children}
+
+ ); +}; + +export default mainLayout; diff --git a/app/(main)/schedule/page.tsx b/app/(main)/schedule/page.tsx index bc18a2d..9ca7f29 100644 --- a/app/(main)/schedule/page.tsx +++ b/app/(main)/schedule/page.tsx @@ -1,7 +1,7 @@ -import React from "react"; - -const page = () => { - return
Schedule Page
; -}; - -export default page; +import React from "react"; + +const page = () => { + return
Schedule Page
; +}; + +export default page; diff --git a/app/(main)/trending/page.tsx b/app/(main)/trending/page.tsx index fcb174d..60f8285 100644 --- a/app/(main)/trending/page.tsx +++ b/app/(main)/trending/page.tsx @@ -1,7 +1,7 @@ -import React from "react"; - -const page = () => { - return
Trending Page
; -}; - -export default page; +import React from "react"; + +const page = () => { + return
Trending Page
; +}; + +export default page; diff --git a/app/globals.css b/app/globals.css index 5d70e21..1a2ec20 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,5 @@ -@import "tailwindcss"; -@plugin './hero.ts'; -/* Note: You may need to change the path to fit your project structure */ -@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; -@custom-variant dark (&:is(.dark *)); +@import "tailwindcss"; +@plugin './hero.ts'; +/* Note: You may need to change the path to fit your project structure */ +@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; +@custom-variant dark (&:is(.dark *)); diff --git a/app/hero.ts b/app/hero.ts index 4401c38..211033b 100644 --- a/app/hero.ts +++ b/app/hero.ts @@ -1,5 +1,5 @@ -// hero.ts -import { heroui } from "@heroui/react"; -// or import from theme package if you are using individual packages. -// import { heroui } from "@heroui/theme"; -export default heroui(); +// hero.ts +import { heroui } from "@heroui/react"; +// or import from theme package if you are using individual packages. +// import { heroui } from "@heroui/theme"; +export default heroui(); diff --git a/features/auth/lib/getOauthProviderList.ts b/features/auth/lib/getOauthProviderList.ts index 9dfdef1..b1be391 100644 --- a/features/auth/lib/getOauthProviderList.ts +++ b/features/auth/lib/getOauthProviderList.ts @@ -1,13 +1,13 @@ -"use server"; -import { api } from "@/shared/lib/ky/connector"; - -const getOauthProviderList = async () => { - try { - const res = await api.get(`auth/providers`); - return res.json(); - } catch (error) { - throw new Error("Failed to fetch OAuth providers", { cause: error }); - } -}; - -export default getOauthProviderList; +"use server"; +import { api } from "@/shared/lib/ky/connector"; + +const getOauthProviderList = async () => { + try { + const res = await api.get(`auth/providers`); + return res.json(); + } catch (error) { + throw new Error("Failed to fetch OAuth providers", { cause: error }); + } +}; + +export default getOauthProviderList; diff --git a/features/auth/lib/requestOauthUrl.ts b/features/auth/lib/requestOauthUrl.ts index 0b38398..9068060 100644 --- a/features/auth/lib/requestOauthUrl.ts +++ b/features/auth/lib/requestOauthUrl.ts @@ -1,36 +1,36 @@ -"use server"; - -import { api } from "@/shared/lib/ky/connector"; -import { redirect } from "next/navigation"; -import { ResponseRequestOauthUrl } from "../types/responseRequestOauthUrl"; - -const requestOauthUrl = async (providerData: { - name: string; - endpoint: string; -}) => { - // Check if requestEndpoint is provided, if not throw an error - if (!providerData.endpoint) - throw new Error("oAuth endpoint request not found"); - - // Define a variable to hold the OAuth data - let oauthData: Promise; - - // Fetch OAuth data from the API - try { - const response = await api.get(providerData.endpoint, { - searchParams: { - callback: `${ - process.env.APP_DOMAIN - }/auth/callback/${providerData.name.toLocaleLowerCase()}`, - }, - }); - oauthData = response.json(); - } catch (error) { - throw new Error(JSON.stringify(error)); - } - - // Redirect to the OAuth provider's authorization page - redirect((await oauthData).data); -}; - -export default requestOauthUrl; +"use server"; + +import { api } from "@/shared/lib/ky/connector"; +import { redirect } from "next/navigation"; +import { ResponseRequestOauthUrl } from "../types/responseRequestOauthUrl"; + +const requestOauthUrl = async (providerData: { + name: string; + endpoint: string; +}) => { + // Check if requestEndpoint is provided, if not throw an error + if (!providerData.endpoint) + throw new Error("oAuth endpoint request not found"); + + // Define a variable to hold the OAuth data + let oauthData: Promise; + + // Fetch OAuth data from the API + try { + const response = await api.get(providerData.endpoint, { + searchParams: { + callback: `${ + process.env.APP_DOMAIN + }/auth/callback/${providerData.name.toLocaleLowerCase()}`, + }, + }); + oauthData = response.json(); + } catch (error) { + throw new Error(JSON.stringify(error)); + } + + // Redirect to the OAuth provider's authorization page + redirect((await oauthData).data); +}; + +export default requestOauthUrl; diff --git a/features/auth/lib/submitRegisterForm.ts b/features/auth/lib/submitRegisterForm.ts index f6dddee..6ddcd47 100644 --- a/features/auth/lib/submitRegisterForm.ts +++ b/features/auth/lib/submitRegisterForm.ts @@ -1,27 +1,27 @@ -"use server"; - -import { apiErrorHandler } from "@/shared/lib/ky/errorHandler"; -import { RegisterInputs } from "../ui/components/ProvisionInput"; -import { ServerRequestCallback } from "@/shared/types/ServerRequestCallback"; - -export const submitRegisterForm = async ( - data: RegisterInputs -): Promise => { - if (data.password !== data.confirmPassword) - return apiErrorHandler([], { - success: false, - status: 400, - text: { message: "Password and Confirm Password do not match" }, - }); - - try { - await new Promise((resolve) => setTimeout(resolve, 3000)); - return { - success: true, - status: 200, - text: { message: "Registration successful" }, - }; - } catch (error) { - return apiErrorHandler(error); - } -}; +"use server"; + +import { apiErrorHandler } from "@/shared/lib/ky/errorHandler"; +import { RegisterInputs } from "../ui/components/ProvisionInput"; +import { ServerRequestCallback } from "@/shared/types/ServerRequestCallback"; + +export const submitRegisterForm = async ( + data: RegisterInputs +): Promise => { + if (data.password !== data.confirmPassword) + return apiErrorHandler([], { + success: false, + status: 400, + text: { message: "Password and Confirm Password do not match" }, + }); + + try { + await new Promise((resolve) => setTimeout(resolve, 3000)); + return { + success: true, + status: 200, + text: { message: "Registration successful" }, + }; + } catch (error) { + return apiErrorHandler(error); + } +}; diff --git a/features/auth/models/registerForm.schema.ts b/features/auth/models/registerForm.schema.ts new file mode 100644 index 0000000..8061864 --- /dev/null +++ b/features/auth/models/registerForm.schema.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +export const registerFormSchema = z + .object({ + fullname: z.string().min(1, "Full name is required"), + email: z.email("Invalid email address"), + password: z + .string() + .min(8, "Password must be at least 8 characters long") + .max(25, "Password must be at most 25 characters long"), + confirmPassword: z + .string() + .min(8, "Password must be at least 8 characters long") + .max(25, "Password must be at most 25 characters long"), + }) + .refine((data) => data.password === data.confirmPassword, { + message: "Passwords confirmation does not match", + path: ["confirmPassword"], + }); + +export type RegisterFormSchema = z.infer; diff --git a/features/auth/pages/LoginPage.tsx b/features/auth/pages/LoginPage.tsx index 0456b53..77054b2 100644 --- a/features/auth/pages/LoginPage.tsx +++ b/features/auth/pages/LoginPage.tsx @@ -1,53 +1,53 @@ -"use client"; - -import { redirect } from "next/navigation"; -import React, { useEffect, useState } from "react"; -import Login from "@/features/auth/ui/cards/Login"; -import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; -import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; - -const LoginPage = () => { - /** - * Create a lit component that will be used in popp, consisting of 3 component flows: - * 1. When the user opens it, a browser environment check will be performed. - * 2. If it passes, the login component will appear and the user will perform authentication. - * 3. If it fails, stop the authentication process and display the warning component, then return the user to the homepage. - */ - const componentFlowList = { - securityCheckup: , - securityCheckupFailed: , - SecurityCheckupSuccessed: , - }; - - // State to set the current page component - const [componentFlow, setComponentFlow] = useState( - componentFlowList.securityCheckup - ); - - useEffect(() => { - /** - * Check if the window has an opener (i.e., it was opened by another window) - * If it does, the security checkup has passed. - * If it doesn't, the security checkup has failed and user will be redirected to the homepage. - */ - if (window.opener) { - setComponentFlow(componentFlowList.SecurityCheckupSuccessed); - } else { - setComponentFlow(componentFlowList.securityCheckupFailed); - - const timer = setTimeout(() => { - redirect("/"); - }, 5000); - return () => clearTimeout(timer); - } - }, []); - - return ( - <> - {/* show the current component flow */} -
{componentFlow}
- - ); -}; - -export default LoginPage; +"use client"; + +import { redirect } from "next/navigation"; +import React, { useEffect, useState } from "react"; +import Login from "@/features/auth/ui/cards/Login"; +import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; +import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; + +const LoginPage = () => { + /** + * Create a lit component that will be used in popp, consisting of 3 component flows: + * 1. When the user opens it, a browser environment check will be performed. + * 2. If it passes, the login component will appear and the user will perform authentication. + * 3. If it fails, stop the authentication process and display the warning component, then return the user to the homepage. + */ + const componentFlowList = { + securityCheckup: , + securityCheckupFailed: , + SecurityCheckupSuccessed: , + }; + + // State to set the current page component + const [componentFlow, setComponentFlow] = useState( + componentFlowList.securityCheckup + ); + + useEffect(() => { + /** + * Check if the window has an opener (i.e., it was opened by another window) + * If it does, the security checkup has passed. + * If it doesn't, the security checkup has failed and user will be redirected to the homepage. + */ + if (window.opener) { + setComponentFlow(componentFlowList.SecurityCheckupSuccessed); + } else { + setComponentFlow(componentFlowList.securityCheckupFailed); + + const timer = setTimeout(() => { + redirect("/"); + }, 5000); + return () => clearTimeout(timer); + } + }, []); + + return ( + <> + {/* show the current component flow */} +
{componentFlow}
+ + ); +}; + +export default LoginPage; diff --git a/features/auth/pages/SignupPage.tsx b/features/auth/pages/SignupPage.tsx index 257bae8..e87c23f 100644 --- a/features/auth/pages/SignupPage.tsx +++ b/features/auth/pages/SignupPage.tsx @@ -1,51 +1,51 @@ -"use client"; - -import { redirect } from "next/navigation"; -import React, { JSX, useEffect, useState } from "react"; -import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; -import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; -import Signup from "../ui/cards/Signup"; - -const SignupPage = () => { - // State to set the current page component - const [componentFlow, setComponentFlow] = useState( - - ); - - /** - * Create a lit component that will be used in popp, consisting of 3 component flows: - * 1. If it passes, the login component will appear and the user will perform authentication. - * 2. If it fails, stop the authentication process and display the warning component, then return the user to the homepage. - */ - const componentFlowList = { - securityCheckupFailed: , - SecurityCheckupSuccessed: , - }; - - useEffect(() => { - /** - * Check if the window has an opener (i.e., it was opened by another window) - * If it does, the security checkup has passed. - * If it doesn't, the security checkup has failed and user will be redirected to the homepage. - */ - if (window.opener) { - setComponentFlow(componentFlowList.SecurityCheckupSuccessed); - } else { - setComponentFlow(componentFlowList.securityCheckupFailed); - - const timer = setTimeout(() => { - redirect("/"); - }, 5000); - return () => clearTimeout(timer); - } - }, []); - - return ( - <> - {/* show the current component flow */} -
{componentFlow}
- - ); -}; - -export default SignupPage; +"use client"; + +import { redirect } from "next/navigation"; +import React, { JSX, useEffect, useState } from "react"; +import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; +import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; +import Signup from "../ui/cards/Signup"; + +const SignupPage = () => { + // State to set the current page component + const [componentFlow, setComponentFlow] = useState( + + ); + + /** + * Create a lit component that will be used in popp, consisting of 3 component flows: + * 1. If it passes, the login component will appear and the user will perform authentication. + * 2. If it fails, stop the authentication process and display the warning component, then return the user to the homepage. + */ + const componentFlowList = { + securityCheckupFailed: , + SecurityCheckupSuccessed: , + }; + + useEffect(() => { + /** + * Check if the window has an opener (i.e., it was opened by another window) + * If it does, the security checkup has passed. + * If it doesn't, the security checkup has failed and user will be redirected to the homepage. + */ + if (window.opener) { + setComponentFlow(componentFlowList.SecurityCheckupSuccessed); + } else { + setComponentFlow(componentFlowList.securityCheckupFailed); + + const timer = setTimeout(() => { + redirect("/"); + }, 5000); + return () => clearTimeout(timer); + } + }, []); + + return ( + <> + {/* show the current component flow */} +
{componentFlow}
+ + ); +}; + +export default SignupPage; diff --git a/features/auth/types/oauthProvidersList.ts b/features/auth/types/oauthProvidersList.ts index 1704e03..cb19c6f 100644 --- a/features/auth/types/oauthProvidersList.ts +++ b/features/auth/types/oauthProvidersList.ts @@ -1,8 +1,8 @@ -export interface OauthProviders { - name: string; - icon: string; - req_endpoint: string; - client_id: string | undefined; - client_secret: string | undefined; - client_callback: string; -} +export interface OauthProviders { + name: string; + icon: string; + req_endpoint: string; + client_id: string | undefined; + client_secret: string | undefined; + client_callback: string; +} diff --git a/features/auth/types/responseRequestOauthUrl.ts b/features/auth/types/responseRequestOauthUrl.ts index af7d111..a060597 100644 --- a/features/auth/types/responseRequestOauthUrl.ts +++ b/features/auth/types/responseRequestOauthUrl.ts @@ -1,5 +1,5 @@ -export interface ResponseRequestOauthUrl { - data: string; - message: string; - status: string; -} +export interface ResponseRequestOauthUrl { + data: string; + message: string; + status: string; +} diff --git a/features/auth/ui/cards/Login.tsx b/features/auth/ui/cards/Login.tsx index 6a605ba..32ccb1e 100644 --- a/features/auth/ui/cards/Login.tsx +++ b/features/auth/ui/cards/Login.tsx @@ -1,42 +1,42 @@ -"use client"; - -import React from "react"; -import { Divider, Link } from "@heroui/react"; -import { routes } from "@/shared/config/routes"; -import EmailInput from "../components/EmailInput"; -import OAuthProviders from "../components/OAuthProviders"; - -const Login = () => { - return ( -
-
Welcome back
- - {/* Email form */} -
- -
- - {/* Sign up link */} -

- Don't have an account?{" "} - - Sign Up - -

- - {/* Divider between email form and third-party login options */} -
- - or - -
- - {/* Buttons for third-party login options */} -
- -
-
- ); -}; - -export default Login; +"use client"; + +import React from "react"; +import { Divider, Link } from "@heroui/react"; +import { routes } from "@/shared/config/routes"; +import EmailInput from "../components/EmailInput"; +import OAuthProviders from "../components/OAuthProviders"; + +const Login = () => { + return ( +
+
Welcome back
+ + {/* Email form */} +
+ +
+ + {/* Sign up link */} +

+ Don't have an account?{" "} + + Sign Up + +

+ + {/* Divider between email form and third-party login options */} +
+ + or + +
+ + {/* Buttons for third-party login options */} +
+ +
+
+ ); +}; + +export default Login; diff --git a/features/auth/ui/cards/Provision.tsx b/features/auth/ui/cards/Provision.tsx index 9b354d6..5b01c9d 100644 --- a/features/auth/ui/cards/Provision.tsx +++ b/features/auth/ui/cards/Provision.tsx @@ -1,22 +1,22 @@ -"use client"; - -import React from "react"; -import ProvisionInput from "../components/ProvisionInput"; - -type Props = { - fullName: string; -}; - -const Provision = ({ fullName }: Props) => { - return ( -
-
Hey, {fullName.split(" ")[0]}
-

- Just a few more steps to join the fun! -

- -
- ); -}; - -export default Provision; +"use client"; + +import React from "react"; +import ProvisionInput from "../components/ProvisionInput"; + +type Props = { + fullName: string; +}; + +const Provision = ({ fullName }: Props) => { + return ( +
+
Hey, {fullName.split(" ")[0]}
+

+ Just a few more steps to join the fun! +

+ +
+ ); +}; + +export default Provision; diff --git a/features/auth/ui/cards/Signup.tsx b/features/auth/ui/cards/Signup.tsx index fb7f4a2..7cf8566 100644 --- a/features/auth/ui/cards/Signup.tsx +++ b/features/auth/ui/cards/Signup.tsx @@ -1,46 +1,46 @@ -"use client"; - -import React from "react"; -import { Divider, Link } from "@heroui/react"; -import { routes } from "@/shared/config/routes"; -import OAuthProviders from "../components/OAuthProviders"; -import FullNameInput from "../components/FullNameInput"; - -type Props = { - changeCurrentPage: React.Dispatch>; -}; - -const Signup = ({ changeCurrentPage }: Props) => { - return ( -
-
Create an account
- - {/* Email form */} -
- -
- - {/* Sign up link */} -

- Already have an account?{" "} - - Log in - -

- - {/* Divider between email form and third-party login options */} -
- - or - -
- - {/* Buttons for third-party login options */} -
- -
-
- ); -}; - -export default Signup; +"use client"; + +import React from "react"; +import { Divider, Link } from "@heroui/react"; +import { routes } from "@/shared/config/routes"; +import OAuthProviders from "../components/OAuthProviders"; +import FullNameInput from "../components/FullNameInput"; + +type Props = { + changeCurrentPage: React.Dispatch>; +}; + +const Signup = ({ changeCurrentPage }: Props) => { + return ( +
+
Create an account
+ + {/* Email form */} +
+ +
+ + {/* Sign up link */} +

+ Already have an account?{" "} + + Log in + +

+ + {/* Divider between email form and third-party login options */} +
+ + or + +
+ + {/* Buttons for third-party login options */} +
+ +
+
+ ); +}; + +export default Signup; diff --git a/features/auth/ui/components/EmailInput.tsx b/features/auth/ui/components/EmailInput.tsx index 053f5ef..606c6ae 100644 --- a/features/auth/ui/components/EmailInput.tsx +++ b/features/auth/ui/components/EmailInput.tsx @@ -1,26 +1,26 @@ -"use client"; - -import { Button, Input } from "@heroui/react"; -import React from "react"; - -const EmailInput = () => { - return ( - <> - - - - ); -}; - -export default EmailInput; +"use client"; + +import { Button, Input } from "@heroui/react"; +import React from "react"; + +const EmailInput = () => { + return ( + <> + + + + ); +}; + +export default EmailInput; diff --git a/features/auth/ui/components/FullNameInput.tsx b/features/auth/ui/components/FullNameInput.tsx index 088593a..048336e 100644 --- a/features/auth/ui/components/FullNameInput.tsx +++ b/features/auth/ui/components/FullNameInput.tsx @@ -1,38 +1,38 @@ -"use client"; - -import React, { useState } from "react"; -import { Button, Input } from "@heroui/react"; -import Provision from "../cards/Provision"; - -type Props = { - changeCurrentPage: React.Dispatch>; -}; - -const FullNameInput = ({ changeCurrentPage }: Props) => { - const [fullName, setFullName] = useState(""); - - return ( - <> - setFullName(e.target.value)} - classNames={{ - input: "text-md font-light pt-4", - inputWrapper: "flex gap-10", - }} - /> - - - ); -}; - -export default FullNameInput; +"use client"; + +import React, { useState } from "react"; +import { Button, Input } from "@heroui/react"; +import Provision from "../cards/Provision"; + +type Props = { + changeCurrentPage: React.Dispatch>; +}; + +const FullNameInput = ({ changeCurrentPage }: Props) => { + const [fullName, setFullName] = useState(""); + + return ( + <> + setFullName(e.target.value)} + classNames={{ + input: "text-md font-light pt-4", + inputWrapper: "flex gap-10", + }} + /> + + + ); +}; + +export default FullNameInput; diff --git a/features/auth/ui/components/OAuthProviders.tsx b/features/auth/ui/components/OAuthProviders.tsx index 1bfbb4e..2072282 100644 --- a/features/auth/ui/components/OAuthProviders.tsx +++ b/features/auth/ui/components/OAuthProviders.tsx @@ -1,85 +1,85 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { OauthProviders } from "../../types/oauthProvidersList"; -import { ResponseRequestOauthUrl } from "../../types/responseRequestOauthUrl"; -import { Button } from "@heroui/react"; -import { Icon } from "@iconify/react"; -import getOauthProviderList from "../../lib/getOauthProviderList"; -import requestOauthUrl from "../../lib/requestOauthUrl"; - -const OAuthProviders = () => { - // Set initial state for OAuth providers list - const [oauthProvidersList, setOauthProvidersList] = useState< - OauthProviders[] - >([]); - - /** - * Fetch the list of OAuth providers from backend API - * and update the state if OAuth providers list is available - */ - useEffect(() => { - (async () => { - try { - const res = (await getOauthProviderList()) as OauthProviders[]; - setOauthProvidersList(res); - } catch (err) { - console.error(err); - } - })(); - }, []); - - const [loadingButton, setLoadingButton] = useState(false); - - /** - * Start the authentication process using oAuth by sending the endpoint URL to the backend for processing. - * - * @param providerRequestEndpoint The request endpoint for the OAuth provider - */ - const startOauthProcess = async (providerData: { - name: string; - endpoint: string; - }) => { - try { - setLoadingButton(true); - - (await requestOauthUrl(providerData)) as ResponseRequestOauthUrl; - } catch (err) { - setLoadingButton(false); - console.error(err); - } - }; - - return ( -
- {/* Render OAuth provider buttons */} - {oauthProvidersList.length > 0 ? ( - oauthProvidersList.map((provider, index) => { - return ( - - ); - }) - ) : ( - - )} -
- ); -}; - -export default OAuthProviders; +"use client"; + +import React, { useEffect, useState } from "react"; +import { OauthProviders } from "../../types/oauthProvidersList"; +import { ResponseRequestOauthUrl } from "../../types/responseRequestOauthUrl"; +import { Button } from "@heroui/react"; +import { Icon } from "@iconify/react"; +import getOauthProviderList from "../../lib/getOauthProviderList"; +import requestOauthUrl from "../../lib/requestOauthUrl"; + +const OAuthProviders = () => { + // Set initial state for OAuth providers list + const [oauthProvidersList, setOauthProvidersList] = useState< + OauthProviders[] + >([]); + + /** + * Fetch the list of OAuth providers from backend API + * and update the state if OAuth providers list is available + */ + useEffect(() => { + (async () => { + try { + const res = (await getOauthProviderList()) as OauthProviders[]; + setOauthProvidersList(res); + } catch (err) { + console.error(err); + } + })(); + }, []); + + const [loadingButton, setLoadingButton] = useState(false); + + /** + * Start the authentication process using oAuth by sending the endpoint URL to the backend for processing. + * + * @param providerRequestEndpoint The request endpoint for the OAuth provider + */ + const startOauthProcess = async (providerData: { + name: string; + endpoint: string; + }) => { + try { + setLoadingButton(true); + + (await requestOauthUrl(providerData)) as ResponseRequestOauthUrl; + } catch (err) { + setLoadingButton(false); + console.error(err); + } + }; + + return ( +
+ {/* Render OAuth provider buttons */} + {oauthProvidersList.length > 0 ? ( + oauthProvidersList.map((provider, index) => { + return ( + + ); + }) + ) : ( + + )} +
+ ); +}; + +export default OAuthProviders; diff --git a/features/auth/ui/components/ProvisionInput.tsx b/features/auth/ui/components/ProvisionInput.tsx index 7a4b2bb..66cef51 100644 --- a/features/auth/ui/components/ProvisionInput.tsx +++ b/features/auth/ui/components/ProvisionInput.tsx @@ -1,103 +1,118 @@ -"use client"; - -import React, { useState } from "react"; -import { addToast, Button, Form, Input } from "@heroui/react"; -import { SubmitHandler, useForm } from "react-hook-form"; -import { submitRegisterForm } from "../../lib/submitRegisterForm"; - -type Props = { - fullname: string; -}; - -export type RegisterInputs = { - fullname: string; - email: string; - password: string; - confirmPassword: string; -}; - -const ProvisionInput = ({ fullname }: Props) => { - const { register, handleSubmit, setValue } = useForm(); - setValue("fullname", fullname); - - const [submitStatus, setSubmitStatus] = useState(false); - const onSubmit: SubmitHandler = async (data) => { - setSubmitStatus(true); - - try { - const returnData = await submitRegisterForm(data); - if (!returnData.success) { - setSubmitStatus(false); - addToast({ - color: "danger", - title: "😬 Oops, something went wrong!", - description: returnData.text.message, - }); - } else { - setSubmitStatus(false); - addToast({ - color: "success", - title: "OKKE!", - description: returnData.text.message, - }); - } - } catch (error) { - setSubmitStatus(false); - addToast({ - color: "danger", - title: "😬 Oops, something went wrong!", - description: "Internal server error", - }); - } - }; - - return ( -
-
- - - - -
-
- ); -}; - -export default ProvisionInput; +"use client"; + +import React, { useState } from "react"; +import { addToast, Button, Form, Input } from "@heroui/react"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { submitRegisterForm } from "../../lib/submitRegisterForm"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { registerFormSchema } from "../../models/registerForm.schema"; + +type Props = { + fullname: string; +}; + +export type RegisterInputs = { + fullname: string; + email: string; + password: string; + confirmPassword: string; +}; + +const ProvisionInput = ({ fullname }: Props) => { + const { + register, + handleSubmit, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerFormSchema), + }); + setValue("fullname", fullname); + + const [submitStatus, setSubmitStatus] = useState(false); + const onSubmit: SubmitHandler = async (data) => { + setSubmitStatus(true); + + try { + const returnData = await submitRegisterForm(data); + if (!returnData.success) { + setSubmitStatus(false); + addToast({ + color: "danger", + title: "😬 Oops, something went wrong!", + description: returnData.text.message, + }); + } else { + setSubmitStatus(false); + addToast({ + color: "success", + title: "OKKE!", + description: returnData.text.message, + }); + } + } catch (error) { + setSubmitStatus(false); + addToast({ + color: "danger", + title: "😬 Oops, something went wrong!", + description: "Connection to server lost", + }); + } + }; + + return ( +
+
+ + + + +
+
+ ); +}; + +export default ProvisionInput; diff --git a/features/oauth-callback/lib/sendCallbackToServer.ts b/features/oauth-callback/lib/sendCallbackToServer.ts index 2e01020..4637b41 100644 --- a/features/oauth-callback/lib/sendCallbackToServer.ts +++ b/features/oauth-callback/lib/sendCallbackToServer.ts @@ -1,83 +1,83 @@ -"use server"; -import { api } from "@/shared/lib/ky/connector"; -import { apiErrorHandler } from "@/shared/lib/ky/errorHandler"; -import { ServerRequestCallback } from "@/shared/types/serverRequestCallback"; - -/** - * @function SendCallbackToServer - * @description Proxies OAuth callback requests from the frontend to the main backend system. - * Acts as an intermediary between the client Next.js application and the Elysia server. - * Handles the forwarding of OAuth provider callback data for authentication processing. - * - * @param {string} data - The OAuth callback data received from the provider, typically containing - * query parameters such as authorization code, user consent, scopes, state, - * and other OAuth-specific information. Usually obtained from - * `window.location.search` in browser environments. - * @param {string} provider - The name of the OAuth provider/service (e.g., "google", "github", - * "facebook"). Used to construct the appropriate backend API endpoint. - * - * @returns {Promise} The response data from the backend server after processing the - * OAuth callback. Typically contains authentication tokens, user - * information, or session data. - * - * @throws {Error} If the network request fails or the backend returns an error response. - * @throws {Error} If the required environment variable APP_DOMAIN is not defined. - * @throws {Error} If the provided parameters are invalid or missing. - * - * @example - * // Handling OAuth callback in a React component - * useEffect(() => { - * const handleOAuthCallback = async () => { - * try { - * const result = await SendCallbackToServer(window.location.search, "google"); - * // Handle successful authentication (e.g., store tokens, redirect user) - * console.log("Authentication successful:", result); - * } catch (error) { - * // Handle authentication errors - * console.error("Authentication failed:", error); - * } - * }; - * - * handleOAuthCallback(); - * }, []); - * - * @example - * // Usage with different providers - * await SendCallbackToServer(window.location.search, "github"); - * await SendCallbackToServer(window.location.search, "facebook"); - * await SendCallbackToServer(window.location.search, "microsoft"); - * - * @remarks - * - This function is specifically designed for OAuth callback handling in a Next.js frontend - * acting as a proxy to an Elysia backend. - * - The `data` parameter should include the complete query string from the OAuth redirect. - * - The callback URI is automatically constructed using the APP_DOMAIN environment variable. - * - Ensure APP_DOMAIN is properly configured in your environment variables. - */ -export const SendCallbackToServer = async ( - data: string, - provider: string -): Promise => { - // Construct the backend and frontend handler URLs - const backendHandlerUrl = `auth/${provider}/callback/`; - const frontendHandlerUrl = `${process.env - .APP_DOMAIN!}/auth/callback/${provider}`; - - try { - // Forward the OAuth callback data to the backend for processing - const response = await api.get( - `${backendHandlerUrl}${data}&callbackURI=${frontendHandlerUrl}` - ); - - // Parse the JSON response from the backend and return the result - const result = await response.json(); - return { - success: true, - status: response.status, - text: { message: "Callback processed successfully" }, - data: result, - }; - } catch (error) { - return apiErrorHandler(error); - } -}; +"use server"; +import { api } from "@/shared/lib/ky/connector"; +import { apiErrorHandler } from "@/shared/lib/ky/errorHandler"; +import { ServerRequestCallback } from "@/shared/types/serverRequestCallback"; + +/** + * @function SendCallbackToServer + * @description Proxies OAuth callback requests from the frontend to the main backend system. + * Acts as an intermediary between the client Next.js application and the Elysia server. + * Handles the forwarding of OAuth provider callback data for authentication processing. + * + * @param {string} data - The OAuth callback data received from the provider, typically containing + * query parameters such as authorization code, user consent, scopes, state, + * and other OAuth-specific information. Usually obtained from + * `window.location.search` in browser environments. + * @param {string} provider - The name of the OAuth provider/service (e.g., "google", "github", + * "facebook"). Used to construct the appropriate backend API endpoint. + * + * @returns {Promise} The response data from the backend server after processing the + * OAuth callback. Typically contains authentication tokens, user + * information, or session data. + * + * @throws {Error} If the network request fails or the backend returns an error response. + * @throws {Error} If the required environment variable APP_DOMAIN is not defined. + * @throws {Error} If the provided parameters are invalid or missing. + * + * @example + * // Handling OAuth callback in a React component + * useEffect(() => { + * const handleOAuthCallback = async () => { + * try { + * const result = await SendCallbackToServer(window.location.search, "google"); + * // Handle successful authentication (e.g., store tokens, redirect user) + * console.log("Authentication successful:", result); + * } catch (error) { + * // Handle authentication errors + * console.error("Authentication failed:", error); + * } + * }; + * + * handleOAuthCallback(); + * }, []); + * + * @example + * // Usage with different providers + * await SendCallbackToServer(window.location.search, "github"); + * await SendCallbackToServer(window.location.search, "facebook"); + * await SendCallbackToServer(window.location.search, "microsoft"); + * + * @remarks + * - This function is specifically designed for OAuth callback handling in a Next.js frontend + * acting as a proxy to an Elysia backend. + * - The `data` parameter should include the complete query string from the OAuth redirect. + * - The callback URI is automatically constructed using the APP_DOMAIN environment variable. + * - Ensure APP_DOMAIN is properly configured in your environment variables. + */ +export const SendCallbackToServer = async ( + data: string, + provider: string +): Promise => { + // Construct the backend and frontend handler URLs + const backendHandlerUrl = `auth/${provider}/callback/`; + const frontendHandlerUrl = `${process.env + .APP_DOMAIN!}/auth/callback/${provider}`; + + try { + // Forward the OAuth callback data to the backend for processing + const response = await api.get( + `${backendHandlerUrl}${data}&callbackURI=${frontendHandlerUrl}` + ); + + // Parse the JSON response from the backend and return the result + const result = await response.json(); + return { + success: true, + status: response.status, + text: { message: "Callback processed successfully" }, + data: result, + }; + } catch (error) { + return apiErrorHandler(error); + } +}; diff --git a/features/oauth-callback/pages/callbackHandler.tsx b/features/oauth-callback/pages/callbackHandler.tsx index e38ee3b..39f64bc 100644 --- a/features/oauth-callback/pages/callbackHandler.tsx +++ b/features/oauth-callback/pages/callbackHandler.tsx @@ -1,56 +1,56 @@ -"use client"; - -import { redirect } from "next/navigation"; -import React, { useEffect, useState } from "react"; -import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; -import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; -import LoadingProcess from "../ui/LoadingProcess"; - -const OauthCallbackHandler = () => { - /** - * Create a lit component that will be used in popp, consisting of 3 component flows: - * 1. When the user opens it, a browser environment check will be performed. - * 2. If it passes, the login component will appear and the user will perform authentication. - * 3. If it fails, stop the authentication process and display the warning component, then return the user to the homepage. - */ - const componentFlowList = { - securityCheckup: , - securityCheckupFailed: , - proceedCallback: , - }; - - // State to set the current page component - const [componentFlow, setComponentFlow] = useState( - componentFlowList.securityCheckup - ); - - useEffect(() => { - // Prevent opening devtools while in authentication page - // disableDevtool(); - - /** - * Check if the window has an opener (i.e., it was opened by another window) - * If it does, the security checkup has passed. - * If it doesn't, the security checkup has failed and user will be redirected to the homepage. - */ - if (window.opener) { - setComponentFlow(componentFlowList.proceedCallback); - } else { - setComponentFlow(componentFlowList.securityCheckupFailed); - - const timer = setTimeout(() => { - redirect("/"); - }, 5000); - return () => clearTimeout(timer); - } - }, []); - - return ( - <> - {/* show the current component flow */} -
{componentFlow}
- - ); -}; - -export default OauthCallbackHandler; +"use client"; + +import { redirect } from "next/navigation"; +import React, { useEffect, useState } from "react"; +import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; +import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; +import LoadingProcess from "../ui/LoadingProcess"; + +const OauthCallbackHandler = () => { + /** + * Create a lit component that will be used in popp, consisting of 3 component flows: + * 1. When the user opens it, a browser environment check will be performed. + * 2. If it passes, the login component will appear and the user will perform authentication. + * 3. If it fails, stop the authentication process and display the warning component, then return the user to the homepage. + */ + const componentFlowList = { + securityCheckup: , + securityCheckupFailed: , + proceedCallback: , + }; + + // State to set the current page component + const [componentFlow, setComponentFlow] = useState( + componentFlowList.securityCheckup + ); + + useEffect(() => { + // Prevent opening devtools while in authentication page + // disableDevtool(); + + /** + * Check if the window has an opener (i.e., it was opened by another window) + * If it does, the security checkup has passed. + * If it doesn't, the security checkup has failed and user will be redirected to the homepage. + */ + if (window.opener) { + setComponentFlow(componentFlowList.proceedCallback); + } else { + setComponentFlow(componentFlowList.securityCheckupFailed); + + const timer = setTimeout(() => { + redirect("/"); + }, 5000); + return () => clearTimeout(timer); + } + }, []); + + return ( + <> + {/* show the current component flow */} +
{componentFlow}
+ + ); +}; + +export default OauthCallbackHandler; diff --git a/features/oauth-callback/types/ParamProps.ts b/features/oauth-callback/types/ParamProps.ts index a0c74af..a06ad16 100644 --- a/features/oauth-callback/types/ParamProps.ts +++ b/features/oauth-callback/types/ParamProps.ts @@ -1,9 +1,9 @@ -export interface ParamProps { - params: { provider: string[] }; - searchParams: - | string - | string[][] - | Record - | URLSearchParams - | undefined; -} +export interface ParamProps { + params: { provider: string[] }; + searchParams: + | string + | string[][] + | Record + | URLSearchParams + | undefined; +} diff --git a/features/oauth-callback/ui/LoadingProcess.tsx b/features/oauth-callback/ui/LoadingProcess.tsx index fbb3667..076e270 100644 --- a/features/oauth-callback/ui/LoadingProcess.tsx +++ b/features/oauth-callback/ui/LoadingProcess.tsx @@ -1,73 +1,73 @@ -"use client"; - -import React from "react"; -import { addToast, Button, CircularProgress, Link } from "@heroui/react"; -import { SendCallbackToServer } from "../lib/sendCallbackToServer"; -import { useParams } from "next/navigation"; -import { useRunOnce } from "@/shared/hooks/useRunOnce"; -import { routes } from "@/shared/config/routes"; - -const LoadingProcess = () => { - // Access the URL parameters - const params = useParams(); - - // Forward the callback response to the backend server - useRunOnce("forwardCallbackResponseToBackend", async () => { - try { - const response = await SendCallbackToServer( - window.location.search, - params.provider as string - ); - - if (response.success) { - window.close(); - } else { - addToast({ - title: "😬 Oops, there's a problem!", - description: response.text.message, - color: "danger", - timeout: 0, - endContent: ( - - ), - }); - } - } catch (error) { - console.log(error); - addToast({ - title: "😵‍💫 Oops, lost connection!", - description: "Check your internet and try again", - color: "danger", - timeout: 0, - endContent: ( - - ), - }); - } - }); - return ( -
- -
-

Please wait...

-

- Your request is being processed -

-
-
- ); -}; - -export default LoadingProcess; +"use client"; + +import React from "react"; +import { addToast, Button, CircularProgress, Link } from "@heroui/react"; +import { SendCallbackToServer } from "../lib/sendCallbackToServer"; +import { useParams } from "next/navigation"; +import { useRunOnce } from "@/shared/hooks/useRunOnce"; +import { routes } from "@/shared/config/routes"; + +const LoadingProcess = () => { + // Access the URL parameters + const params = useParams(); + + // Forward the callback response to the backend server + useRunOnce("forwardCallbackResponseToBackend", async () => { + try { + const response = await SendCallbackToServer( + window.location.search, + params.provider as string + ); + + if (response.success) { + window.close(); + } else { + addToast({ + title: "😬 Oops, there's a problem!", + description: response.text.message, + color: "danger", + timeout: 0, + endContent: ( + + ), + }); + } + } catch (error) { + console.log(error); + addToast({ + title: "😵‍💫 Oops, lost connection!", + description: "Check your internet and try again", + color: "danger", + timeout: 0, + endContent: ( + + ), + }); + } + }); + return ( +
+ +
+

Please wait...

+

+ Your request is being processed +

+
+
+ ); +}; + +export default LoadingProcess; diff --git a/next.config.ts b/next.config.ts index 1548f9e..a6b224f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,42 +1,42 @@ -import {withSentryConfig} from "@sentry/nextjs"; -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - /* config options here */ - env: { - NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN, - } -}; - -export default withSentryConfig(nextConfig, { - // For all available options, see: - // https://www.npmjs.com/package/@sentry/webpack-plugin#options - - org: process.env.NEXT_PUBLIC_SENTRY_ORG || "", - project: process.env.NEXT_PUBLIC_SENTRY_PROJECT || "", - sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL || "", - - // Only print logs for uploading source maps in CI - silent: !process.env.CI, - - // For all available options, see: - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - - // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, - - // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. - // This can increase your server load as well as your hosting bill. - // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- - // side errors will fail. - tunnelRoute: "/monitoring", - - // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, - - // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) - // See the following for more information: - // https://docs.sentry.io/product/crons/ - // https://vercel.com/docs/cron-jobs - automaticVercelMonitors: true +import {withSentryConfig} from "@sentry/nextjs"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ + env: { + NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN, + } +}; + +export default withSentryConfig(nextConfig, { + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + + org: process.env.NEXT_PUBLIC_SENTRY_ORG || "", + project: process.env.NEXT_PUBLIC_SENTRY_PROJECT || "", + sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL || "", + + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: "/monitoring", + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + + // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) + // See the following for more information: + // https://docs.sentry.io/product/crons/ + // https://vercel.com/docs/cron-jobs + automaticVercelMonitors: true }); \ No newline at end of file diff --git a/postcss.config.mjs b/postcss.config.mjs index 7059fe9..f10c266 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,6 +1,6 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; -export default config; +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +export default config; diff --git a/providers/HeroUIWrapper.tsx b/providers/HeroUIWrapper.tsx index 9eba17e..cd75ab1 100644 --- a/providers/HeroUIWrapper.tsx +++ b/providers/HeroUIWrapper.tsx @@ -1,32 +1,32 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { HeroUIProvider, ToastProvider } from "@heroui/react"; -import { ThemeProvider as NextThemesProvider } from "next-themes"; -import { useRouter } from "next/navigation"; - -const HeroUIWrapper = ({ - children, -}: Readonly<{ children: React.ReactNode }>) => { - const [mounted, setMounted] = useState(false); - useEffect(() => { - setMounted(true); - }, []); - - const router = useRouter(); - - return ( - - - {mounted ? ( - -
{children}
-
- ) : ( -
{children}
- )} -
- ); -}; - -export default HeroUIWrapper; +"use client"; + +import React, { useEffect, useState } from "react"; +import { HeroUIProvider, ToastProvider } from "@heroui/react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { useRouter } from "next/navigation"; + +const HeroUIWrapper = ({ + children, +}: Readonly<{ children: React.ReactNode }>) => { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + const router = useRouter(); + + return ( + + + {mounted ? ( + +
{children}
+
+ ) : ( +
{children}
+ )} +
+ ); +}; + +export default HeroUIWrapper; diff --git a/providers/fonts/GeistFontProvider.tsx b/providers/fonts/GeistFontProvider.tsx index 1788c27..991c930 100644 --- a/providers/fonts/GeistFontProvider.tsx +++ b/providers/fonts/GeistFontProvider.tsx @@ -1,60 +1,60 @@ -import React from "react"; -import localFont from "next/font/local"; - -const Geist = localFont({ - src: [ - { - path: "../../fonts/Geist-Black.ttf", - weight: "900", - style: "normal", - }, - { - path: "../../fonts/Geist-ExtraBold.ttf", - weight: "800", - style: "normal", - }, - { - path: "../../fonts/Geist-Bold.ttf", - weight: "700", - style: "normal", - }, - { - path: "../../fonts/Geist-SemiBold.ttf", - weight: "600", - style: "normal", - }, - { - path: "../../fonts/Geist-Medium.ttf", - weight: "500", - style: "normal", - }, - { - path: "../../fonts/Geist-Regular.ttf", - weight: "400", - style: "normal", - }, - { - path: "../../fonts/Geist-Light.ttf", - weight: "300", - style: "normal", - }, - { - path: "../../fonts/Geist-ExtraLight.ttf", - weight: "200", - style: "normal", - }, - { - path: "../../fonts/Geist-Thin.ttf", - weight: "100", - style: "normal", - }, - ], -}); - -const GeistFontProvider = ({ - children, -}: Readonly<{ children: React.ReactNode }>) => { - return
{children}
; -}; - -export default GeistFontProvider; +import React from "react"; +import localFont from "next/font/local"; + +const Geist = localFont({ + src: [ + { + path: "../../fonts/Geist-Black.ttf", + weight: "900", + style: "normal", + }, + { + path: "../../fonts/Geist-ExtraBold.ttf", + weight: "800", + style: "normal", + }, + { + path: "../../fonts/Geist-Bold.ttf", + weight: "700", + style: "normal", + }, + { + path: "../../fonts/Geist-SemiBold.ttf", + weight: "600", + style: "normal", + }, + { + path: "../../fonts/Geist-Medium.ttf", + weight: "500", + style: "normal", + }, + { + path: "../../fonts/Geist-Regular.ttf", + weight: "400", + style: "normal", + }, + { + path: "../../fonts/Geist-Light.ttf", + weight: "300", + style: "normal", + }, + { + path: "../../fonts/Geist-ExtraLight.ttf", + weight: "200", + style: "normal", + }, + { + path: "../../fonts/Geist-Thin.ttf", + weight: "100", + style: "normal", + }, + ], +}); + +const GeistFontProvider = ({ + children, +}: Readonly<{ children: React.ReactNode }>) => { + return
{children}
; +}; + +export default GeistFontProvider; diff --git a/scripts/create-env-example.ts b/scripts/create-env-example.ts index 7628755..a3e228d 100644 --- a/scripts/create-env-example.ts +++ b/scripts/create-env-example.ts @@ -1,49 +1,49 @@ -import fs from "fs"; -import path from "path"; - -// These keys will not be cleared in the .env.example file -const PRESERVED_KEYS = ["APP_NAME", "APP_ENV", "APP_PORT"]; - -/** - * Script to create or update the .env.example file based on the .env file. - * It preserves certain keys and clears their values in the .env.example file. - */ -try { - const envPath = path.join(process.cwd(), ".env"); - const envExamplePath = path.join(process.cwd(), ".env.example"); - - if (!fs.existsSync(envPath)) { - console.error(`.env file not found at ${envPath}`); - process.exit(1); - } - - const envContent = fs.readFileSync(envPath, "utf-8"); - - const lines = envContent.split("\n"); - const processedLines = lines.map((line) => { - const trimmedLine = line.trim(); - - if (trimmedLine.startsWith("#") || trimmedLine === "") { - return line; - } - - const delimeterIndex = line.indexOf("="); - if (delimeterIndex === -1) { - return line; - } - - const key = line.substring(0, delimeterIndex).trim(); - const value = line.substring(delimeterIndex + 1).trim(); - - if (PRESERVED_KEYS.includes(key)) { - return `${key}=${value}`; - } - return `${key}=`; - }); - - fs.writeFileSync(envExamplePath, processedLines.join("\n")); - console.log("File .env.example berhasil diperbarui!"); -} catch (error) { - console.error("Error while creating .env.example:", error); - process.exit(1); -} +import fs from "fs"; +import path from "path"; + +// These keys will not be cleared in the .env.example file +const PRESERVED_KEYS = ["APP_NAME", "APP_ENV", "APP_PORT"]; + +/** + * Script to create or update the .env.example file based on the .env file. + * It preserves certain keys and clears their values in the .env.example file. + */ +try { + const envPath = path.join(process.cwd(), ".env"); + const envExamplePath = path.join(process.cwd(), ".env.example"); + + if (!fs.existsSync(envPath)) { + console.error(`.env file not found at ${envPath}`); + process.exit(1); + } + + const envContent = fs.readFileSync(envPath, "utf-8"); + + const lines = envContent.split("\n"); + const processedLines = lines.map((line) => { + const trimmedLine = line.trim(); + + if (trimmedLine.startsWith("#") || trimmedLine === "") { + return line; + } + + const delimeterIndex = line.indexOf("="); + if (delimeterIndex === -1) { + return line; + } + + const key = line.substring(0, delimeterIndex).trim(); + const value = line.substring(delimeterIndex + 1).trim(); + + if (PRESERVED_KEYS.includes(key)) { + return `${key}=${value}`; + } + return `${key}=`; + }); + + fs.writeFileSync(envExamplePath, processedLines.join("\n")); + console.log("File .env.example berhasil diperbarui!"); +} catch (error) { + console.error("Error while creating .env.example:", error); + process.exit(1); +} diff --git a/scripts/git-multipush.example.ts b/scripts/git-multipush.example.ts index 937ceed..3a54d5a 100644 --- a/scripts/git-multipush.example.ts +++ b/scripts/git-multipush.example.ts @@ -1,22 +1,22 @@ -import { execSync } from "child_process"; - -/* -This script pushes the current branch to multiple remotes in a Git repository. -It is useful for deploying code to multiple servers or services at once. -Make sure you've set up your remotes correctly before running this script and do commit your changes first! -*/ - -const remotes = ["origin"]; // Add your remote names here, e.g., "origin", "vps", etc. if you have multiple remotes - -// Start the push process -for (const remote of remotes) { - console.log(`Pushing to ${remote}...`); - try { - execSync(`git push ${remote} main`, { stdio: "inherit" }); - } catch (err) { - console.error(`❌ Failed to push to ${remote}`); - } -} - -// All remotes processed -console.log("✅ All remotes processed."); +import { execSync } from "child_process"; + +/* +This script pushes the current branch to multiple remotes in a Git repository. +It is useful for deploying code to multiple servers or services at once. +Make sure you've set up your remotes correctly before running this script and do commit your changes first! +*/ + +const remotes = ["origin"]; // Add your remote names here, e.g., "origin", "vps", etc. if you have multiple remotes + +// Start the push process +for (const remote of remotes) { + console.log(`Pushing to ${remote}...`); + try { + execSync(`git push ${remote} main`, { stdio: "inherit" }); + } catch (err) { + console.error(`❌ Failed to push to ${remote}`); + } +} + +// All remotes processed +console.log("✅ All remotes processed."); diff --git a/shared/auth/ui/SecurityCheckup.tsx b/shared/auth/ui/SecurityCheckup.tsx index d778297..a794510 100644 --- a/shared/auth/ui/SecurityCheckup.tsx +++ b/shared/auth/ui/SecurityCheckup.tsx @@ -1,17 +1,17 @@ -import React from "react"; - -const SecurityCheckup = () => { - return ( -
-
-

Please wait...

-

- We want to ensure a secure authentication environment before - proceeding for your safety. -

-
-
- ); -}; - -export default SecurityCheckup; +import React from "react"; + +const SecurityCheckup = () => { + return ( +
+
+

Please wait...

+

+ We want to ensure a secure authentication environment before + proceeding for your safety. +

+
+
+ ); +}; + +export default SecurityCheckup; diff --git a/shared/auth/ui/SecurityCheckupFailed.tsx b/shared/auth/ui/SecurityCheckupFailed.tsx index b1b195b..245d5d6 100644 --- a/shared/auth/ui/SecurityCheckupFailed.tsx +++ b/shared/auth/ui/SecurityCheckupFailed.tsx @@ -1,19 +1,19 @@ -import React from "react"; - -const SecurityCheckupFailed = () => { - return ( -
-
-

- Your browser is not secure -

-

- Sorry, we had to stop the authentication process and return you to the - home page because your browser environment is not secure. -

-
-
- ); -}; - -export default SecurityCheckupFailed; +import React from "react"; + +const SecurityCheckupFailed = () => { + return ( +
+
+

+ Your browser is not secure +

+

+ Sorry, we had to stop the authentication process and return you to the + home page because your browser environment is not secure. +

+
+
+ ); +}; + +export default SecurityCheckupFailed; diff --git a/shared/config/meta.ts b/shared/config/meta.ts index c75e69d..3d08c1d 100644 --- a/shared/config/meta.ts +++ b/shared/config/meta.ts @@ -1,29 +1,29 @@ -type BuildMeta = { - title?: string; - description?: string; - image?: string; -}; - -const appName = process.env.APP_NAME; -export const defaultMeta = { - title: appName || "Unknown App", - description: "Interactive community", -}; - -export const buildMeta = ({ title, description, image }: BuildMeta) => { - return { - title: title ? `${title} - ${appName}` : defaultMeta.title, - description: description || defaultMeta.description, - openGraph: { - title, - description, - images: image ? [image] : ["/default-og.png"], - }, - twitter: { - card: "summary_large_image", - title, - description, - images: image ? [image] : ["/default-og.png"], - }, - }; -}; +type BuildMeta = { + title?: string; + description?: string; + image?: string; +}; + +const appName = process.env.APP_NAME; +export const defaultMeta = { + title: appName || "Unknown App", + description: "Interactive community", +}; + +export const buildMeta = ({ title, description, image }: BuildMeta) => { + return { + title: title ? `${title} - ${appName}` : defaultMeta.title, + description: description || defaultMeta.description, + openGraph: { + title, + description, + images: image ? [image] : ["/default-og.png"], + }, + twitter: { + card: "summary_large_image", + title, + description, + images: image ? [image] : ["/default-og.png"], + }, + }; +}; diff --git a/shared/config/routes.ts b/shared/config/routes.ts index d822bec..7f36300 100644 --- a/shared/config/routes.ts +++ b/shared/config/routes.ts @@ -1,9 +1,9 @@ -export const routes = { - home: "/", - login: "/login", - signup: "/signup", - explore: "/explore", - trending: "/trending", - genres: "/genres", - schedule: "/schedule", -}; +export const routes = { + home: "/", + login: "/login", + signup: "/signup", + explore: "/explore", + trending: "/trending", + genres: "/genres", + schedule: "/schedule", +}; diff --git a/shared/helper/backendApi.ts b/shared/helper/backendApi.ts index f6a2fb9..0b2c3f6 100644 --- a/shared/helper/backendApi.ts +++ b/shared/helper/backendApi.ts @@ -1,21 +1,21 @@ -export const API_BASE_URL = - process.env.MAIN_BACKEND_API_URL ?? "http://localhost"; - -const apiFetch = async ( - path: string, - init?: RequestInit -): Promise => { - const res = await fetch(`${API_BASE_URL}${path}`, { - ...init, - headers: { - "Content-Type": "application/json", - ...init?.headers, - }, - cache: "no-store", - }); - - if (!res.ok) throw new Error(await res.text()); - return res.json(); -}; - -export default apiFetch; +export const API_BASE_URL = + process.env.MAIN_BACKEND_API_URL ?? "http://localhost"; + +const apiFetch = async ( + path: string, + init?: RequestInit +): Promise => { + const res = await fetch(`${API_BASE_URL}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...init?.headers, + }, + cache: "no-store", + }); + + if (!res.ok) throw new Error(await res.text()); + return res.json(); +}; + +export default apiFetch; diff --git a/shared/helper/delayButtonClick.ts b/shared/helper/delayButtonClick.ts index 64146e2..152d488 100644 --- a/shared/helper/delayButtonClick.ts +++ b/shared/helper/delayButtonClick.ts @@ -1,11 +1,11 @@ -import { useRouter } from "next/navigation"; - -export const delayButtonClick = ( - router: ReturnType, - href: string, - timeout: number = 300 -) => { - setTimeout(() => { - router.push(href); - }, timeout); -}; +import { useRouter } from "next/navigation"; + +export const delayButtonClick = ( + router: ReturnType, + href: string, + timeout: number = 300 +) => { + setTimeout(() => { + router.push(href); + }, timeout); +}; diff --git a/shared/hooks/useRunOnce.ts b/shared/hooks/useRunOnce.ts index f7dbea5..f44c1c6 100644 --- a/shared/hooks/useRunOnce.ts +++ b/shared/hooks/useRunOnce.ts @@ -1,58 +1,58 @@ -"use client"; -import { useEffect, useRef } from "react"; - -/** - * @function useRunOnce - * @description A custom React hook that ensures a function is executed only once - * across the entire application, even in React Strict Mode or during - * development hot reloads. Maintains a global registry to track - * execution status using a unique key. - * - * Particularly useful for one-time initialization logic, analytics - * tracking, or any operation that should not be duplicated. - * - * @param {string} key - A unique identifier for the process. Used to track execution - * across component instances and prevent naming collisions. - * Should be descriptive and unique (e.g., "user_analytics_init"). - * @param {function} fn - The function to be executed once. Should contain the logic - * that needs to run only a single time. - * - * @returns {void} - * - * @throws {Error} If the provided key is not a string or is empty. - * @throws {Error} If the provided function is not callable. - * - * @example - * // One-time asynchronous operation - * useRunOnce("async_operation", async () => { - * await yourAsyncFunction(); - * }); - * - * @example - * // One-time synchronous operation - * useRunOnce("sync_operation", () => { - * yourFunction(); - * }); - * - * @remarks - * - The hook uses a global registry, so the same key across different components - * will prevent duplicate execution. - * - Safe to use in React Strict Mode and development environment with hot reload. - * - The function will not execute if another instance with the same key has - * already run in the application. - */ -const registry = new Set(); - -export function useRunOnce(key: string, fn: () => void) { - const hasRun = useRef(false); - - useEffect(() => { - if (hasRun.current) return; - hasRun.current = true; - - if (registry.has(key)) return; - registry.add(key); - - fn(); - }, [key, fn]); -} +"use client"; +import { useEffect, useRef } from "react"; + +/** + * @function useRunOnce + * @description A custom React hook that ensures a function is executed only once + * across the entire application, even in React Strict Mode or during + * development hot reloads. Maintains a global registry to track + * execution status using a unique key. + * + * Particularly useful for one-time initialization logic, analytics + * tracking, or any operation that should not be duplicated. + * + * @param {string} key - A unique identifier for the process. Used to track execution + * across component instances and prevent naming collisions. + * Should be descriptive and unique (e.g., "user_analytics_init"). + * @param {function} fn - The function to be executed once. Should contain the logic + * that needs to run only a single time. + * + * @returns {void} + * + * @throws {Error} If the provided key is not a string or is empty. + * @throws {Error} If the provided function is not callable. + * + * @example + * // One-time asynchronous operation + * useRunOnce("async_operation", async () => { + * await yourAsyncFunction(); + * }); + * + * @example + * // One-time synchronous operation + * useRunOnce("sync_operation", () => { + * yourFunction(); + * }); + * + * @remarks + * - The hook uses a global registry, so the same key across different components + * will prevent duplicate execution. + * - Safe to use in React Strict Mode and development environment with hot reload. + * - The function will not execute if another instance with the same key has + * already run in the application. + */ +const registry = new Set(); + +export function useRunOnce(key: string, fn: () => void) { + const hasRun = useRef(false); + + useEffect(() => { + if (hasRun.current) return; + hasRun.current = true; + + if (registry.has(key)) return; + registry.add(key); + + fn(); + }, [key, fn]); +} diff --git a/shared/lib/ky/connector.ts b/shared/lib/ky/connector.ts index aceb73a..6648056 100644 --- a/shared/lib/ky/connector.ts +++ b/shared/lib/ky/connector.ts @@ -1,12 +1,12 @@ -"use server"; - -import ky from "ky"; - -export const api = ky.create({ - prefixUrl: process.env.MAIN_BACKEND_API_URL, - credentials: "include", - headers: { - access_token: process.env.MAIN_BACKEND_API_KEY, - }, - retry: 0, -}); +"use server"; + +import ky from "ky"; + +export const api = ky.create({ + prefixUrl: process.env.MAIN_BACKEND_API_URL, + credentials: "include", + headers: { + access_token: process.env.MAIN_BACKEND_API_KEY, + }, + retry: 0, +}); diff --git a/shared/lib/ky/errorHandler.ts b/shared/lib/ky/errorHandler.ts index e452faa..ddd17b8 100644 --- a/shared/lib/ky/errorHandler.ts +++ b/shared/lib/ky/errorHandler.ts @@ -1,34 +1,34 @@ -"use server"; - -import { HTTPError } from "ky"; - -export type CallApiErrorHandler = { - success?: boolean; - status?: number; - text?: { message?: string }; -}; - -export const apiErrorHandler = async ( - error: unknown, - safeFail?: CallApiErrorHandler -) => { - if (error instanceof HTTPError) { - return { - success: false, - status: error.response.status, - text: await error.response.json(), - }; - } - - if (safeFail) { - return { - success: safeFail.success || false, - status: safeFail.status || 500, - text: { - message: safeFail.text?.message || "An unexpected error occurred", - }, - }; - } else { - throw error; - } -}; +"use server"; + +import { HTTPError } from "ky"; + +export type CallApiErrorHandler = { + success?: boolean; + status?: number; + text?: { message?: string }; +}; + +export const apiErrorHandler = async ( + error: unknown, + safeFail?: CallApiErrorHandler +) => { + if (error instanceof HTTPError) { + return { + success: false, + status: error.response.status, + text: await error.response.json(), + }; + } + + if (safeFail) { + return { + success: safeFail.success || false, + status: safeFail.status || 500, + text: { + message: safeFail.text?.message || "An unexpected error occurred", + }, + }; + } else { + throw error; + } +}; diff --git a/shared/types/ServerRequestCallback.ts b/shared/types/ServerRequestCallback.ts index 7b33c27..b9a2cf9 100644 --- a/shared/types/ServerRequestCallback.ts +++ b/shared/types/ServerRequestCallback.ts @@ -1,7 +1,7 @@ -export type ServerRequestCallback = { - success: boolean; - status: number; - text: { message: string }; - data?: any; - error?: unknown; -}; +export type ServerRequestCallback = { + success: boolean; + status: number; + text: { message: string }; + data?: any; + error?: unknown; +}; diff --git a/widgets/navbar/ui/LoginAndSignup.tsx b/widgets/navbar/ui/LoginAndSignup.tsx index 617c50b..70bd638 100644 --- a/widgets/navbar/ui/LoginAndSignup.tsx +++ b/widgets/navbar/ui/LoginAndSignup.tsx @@ -1,37 +1,37 @@ -"use client"; - -import { routes } from "@/shared/config/routes"; -import { Button, Link, NavbarItem } from "@heroui/react"; -import React from "react"; - -const LoginAndSignup = () => { - const openPopupWindow = (href: string) => { - window.open(href, "popup", "width=500,height=600"); - }; - return ( - <> - - - - - - - - ); -}; - -export default LoginAndSignup; +"use client"; + +import { routes } from "@/shared/config/routes"; +import { Button, Link, NavbarItem } from "@heroui/react"; +import React from "react"; + +const LoginAndSignup = () => { + const openPopupWindow = (href: string) => { + window.open(href, "popup", "width=500,height=600"); + }; + return ( + <> + + + + + + + + ); +}; + +export default LoginAndSignup; diff --git a/widgets/navbar/ui/Navbar.tsx b/widgets/navbar/ui/Navbar.tsx index c5c9587..6e3c5f3 100644 --- a/widgets/navbar/ui/Navbar.tsx +++ b/widgets/navbar/ui/Navbar.tsx @@ -1,111 +1,111 @@ -"use client"; - -import { - Link, - Navbar, - NavbarBrand, - NavbarContent, - NavbarItem, - NavbarMenu, - NavbarMenuItem, - NavbarMenuToggle, -} from "@heroui/react"; -import { usePathname } from "next/navigation"; -import React, { useState } from "react"; -import { routes } from "../../../shared/config/routes"; -import LoginAndSignup from "./LoginAndSignup"; - -export const AcmeLogo = () => { - return ( - - - - ); -}; - -const NavbarUI = () => { - const pathNameNow = usePathname(); - - const [isMenuOpen, setIsMenuOpen] = useState(false); - - const navbarItems = [ - { - title: "Home", - route: routes.home, - }, - { - title: "Explore", - route: routes.explore, - }, - { - title: "Trending", - route: routes.trending, - }, - { - title: "Genres", - route: routes.genres, - }, - { - title: "Schedule", - route: routes.schedule, - }, - ]; - - return ( - - - - - -

ACME

-
-
- - - {navbarItems.map((item, index) => { - const isActive = item.route === pathNameNow; - - return ( - - - {item.title} - - - ); - })} - - - - - - - - {navbarItems.map((item, index) => ( - - - {item.title} - - - ))} - -
- ); -}; - -export default NavbarUI; +"use client"; + +import { + Link, + Navbar, + NavbarBrand, + NavbarContent, + NavbarItem, + NavbarMenu, + NavbarMenuItem, + NavbarMenuToggle, +} from "@heroui/react"; +import { usePathname } from "next/navigation"; +import React, { useState } from "react"; +import { routes } from "../../../shared/config/routes"; +import LoginAndSignup from "./LoginAndSignup"; + +export const AcmeLogo = () => { + return ( + + + + ); +}; + +const NavbarUI = () => { + const pathNameNow = usePathname(); + + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const navbarItems = [ + { + title: "Home", + route: routes.home, + }, + { + title: "Explore", + route: routes.explore, + }, + { + title: "Trending", + route: routes.trending, + }, + { + title: "Genres", + route: routes.genres, + }, + { + title: "Schedule", + route: routes.schedule, + }, + ]; + + return ( + + + + + +

ACME

+
+
+ + + {navbarItems.map((item, index) => { + const isActive = item.route === pathNameNow; + + return ( + + + {item.title} + + + ); + })} + + + + + + + + {navbarItems.map((item, index) => ( + + + {item.title} + + + ))} + +
+ ); +}; + +export default NavbarUI;