🔒 (security) security improvement

This commit is contained in:
2025-10-10 23:57:09 +07:00
parent 54f4e72b32
commit 15c9599ce7
54 changed files with 1603 additions and 1567 deletions

View File

@ -1,28 +1,28 @@
name: Bun CI name: Bun CI
on: on:
push: push:
branches: [main] branches: [main]
pull_request: pull_request:
branches: [main] branches: [main]
jobs: jobs:
build-testing: build-testing:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: container:
image: oven/bun:latest image: oven/bun:latest
steps: steps:
- name: Install Git - name: Install Git
run: apt update && apt install -y git run: apt update && apt install -y git
- name: Clone private repo - name: Clone private repo
run: git clone "$GITEA_REPOSITORY_CLONE_URL" . run: git clone "$GITEA_REPOSITORY_CLONE_URL" .
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
- name: Running lint - name: Running lint
run: bun run lint run: bun run lint
- name: Running build - name: Running build
run: bun run build run: bun run build

View File

@ -1,31 +1,31 @@
name: CI name: CI
on: on:
push: push:
branches: branches:
- main - main
pull_request: pull_request:
branches: branches:
- main - main
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Install Bun - name: Install Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
with: with:
bun-version: latest bun-version: latest
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
- name: Run lint - name: Run lint
run: bun run lint run: bun run lint
- name: Run build - name: Run build
run: bun run build run: bun run build

View File

@ -1,18 +1,18 @@
image: oven/bun:latest image: oven/bun:latest
stages: stages:
- lint - lint
- build - build
before_script: before_script:
- bun install - bun install
lint: lint:
stage: lint stage: lint
script: script:
- bun run lint - bun run lint
build: build:
stage: build stage: build
script: script:
- bun run build - bun run build

View File

@ -1,8 +1,8 @@
import OauthCallbackHandler from "@/features/oauth-callback/pages/callbackHandler"; import OauthCallbackHandler from "@/features/oauth-callback/pages/callbackHandler";
import React from "react"; import React from "react";
const page = () => { const page = () => {
return <OauthCallbackHandler />; return <OauthCallbackHandler />;
}; };
export default page; export default page;

View File

@ -1,5 +1,5 @@
import { Metadata } from "next"; import { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Login | Nounoz TV", title: "Login | Nounoz TV",
}; };

View File

@ -1,11 +1,11 @@
import LoginPage from "@/features/auth/pages/LoginPage"; import LoginPage from "@/features/auth/pages/LoginPage";
import { metadata } from "./metadata"; import { metadata } from "./metadata";
export { metadata }; export { metadata };
import React from "react"; import React from "react";
const page = () => { const page = () => {
return <LoginPage />; return <LoginPage />;
}; };
export default page; export default page;

View File

@ -1,5 +1,5 @@
import { Metadata } from "next"; import { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sign Up | Nounoz TV", title: "Sign Up | Nounoz TV",
}; };

View File

@ -1,11 +1,11 @@
import SignupPage from "@/features/auth/pages/SignupPage"; import SignupPage from "@/features/auth/pages/SignupPage";
import { metadata } from "./metadata"; import { metadata } from "./metadata";
export { metadata }; export { metadata };
import React from "react"; import React from "react";
const page = () => { const page = () => {
return <SignupPage />; return <SignupPage />;
}; };
export default page; export default page;

View File

@ -1,10 +1,10 @@
"use client"; "use client";
import { Button } from "@heroui/react"; import { Button } from "@heroui/react";
import React from "react"; import React from "react";
const button = () => { const button = () => {
return <Button color="primary">Button</Button>; return <Button color="primary">Button</Button>;
}; };
export default button; export default button;

View File

@ -1,39 +1,39 @@
import { Metadata } from "next"; import { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Nounoz TV - Anime Streaming Station Center", title: "Nounoz TV - Anime Streaming Station Center",
description: 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.", "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: [ keywords: [
"nonton anime", "nonton anime",
"streaming anime", "streaming anime",
"anime sub indo", "anime sub indo",
"anime HD", "anime HD",
"komunitas anime", "komunitas anime",
"Nounoz TV", "Nounoz TV",
], ],
openGraph: { openGraph: {
title: "Nounoz TV - Streaming Anime HD + Komunitas Asik", title: "Nounoz TV - Streaming Anime HD + Komunitas Asik",
description: description:
"Nonton anime jadi lebih seru bareng teman-teman. Kualitas jernih, tanpa iklan ganggu, dan selalu update!", "Nonton anime jadi lebih seru bareng teman-teman. Kualitas jernih, tanpa iklan ganggu, dan selalu update!",
url: "https://nounoz.tv", url: "https://nounoz.tv",
siteName: "Nounoz TV", siteName: "Nounoz TV",
images: [ images: [
{ {
url: "https://nounoz.tv/og-image.jpg", url: "https://nounoz.tv/og-image.jpg",
width: 1200, width: 1200,
height: 630, height: 630,
alt: "Nounoz TV - Nonton Anime HD Bareng Komunitas", alt: "Nounoz TV - Nonton Anime HD Bareng Komunitas",
}, },
], ],
locale: "id_ID", locale: "id_ID",
type: "website", type: "website",
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: "Nounoz TV - Nonton Anime HD Bareng Komunitas", title: "Nounoz TV - Nonton Anime HD Bareng Komunitas",
description: description:
"Streaming anime kualitas tinggi sambil ngobrol santai bareng komunitas yang aktif dan suportif.", "Streaming anime kualitas tinggi sambil ngobrol santai bareng komunitas yang aktif dan suportif.",
images: ["https://nounoz.tv/og-image.jpg"], images: ["https://nounoz.tv/og-image.jpg"],
}, },
}; };

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
const page = () => { const page = () => {
return <div>Explore Page</div>; return <div>Explore Page</div>;
}; };
export default page; export default page;

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
const page = () => { const page = () => {
return <div>Genre Page</div>; return <div>Genre Page</div>;
}; };
export default page; export default page;

View File

@ -1,13 +1,13 @@
import NavbarUI from "@/widgets/navbar/ui/Navbar"; import NavbarUI from "@/widgets/navbar/ui/Navbar";
import React from "react"; import React from "react";
const mainLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { const mainLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return ( return (
<div> <div>
<NavbarUI /> <NavbarUI />
<main>{children}</main> <main>{children}</main>
</div> </div>
); );
}; };
export default mainLayout; export default mainLayout;

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
const page = () => { const page = () => {
return <div>Schedule Page</div>; return <div>Schedule Page</div>;
}; };
export default page; export default page;

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
const page = () => { const page = () => {
return <div>Trending Page</div>; return <div>Trending Page</div>;
}; };
export default page; export default page;

View File

@ -1,5 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin './hero.ts'; @plugin './hero.ts';
/* Note: You may need to change the path to fit your project structure */ /* Note: You may need to change the path to fit your project structure */
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; @source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));

View File

@ -1,5 +1,5 @@
// hero.ts // hero.ts
import { heroui } from "@heroui/react"; import { heroui } from "@heroui/react";
// or import from theme package if you are using individual packages. // or import from theme package if you are using individual packages.
// import { heroui } from "@heroui/theme"; // import { heroui } from "@heroui/theme";
export default heroui(); export default heroui();

View File

@ -1,13 +1,13 @@
"use server"; "use server";
import { api } from "@/shared/lib/ky/connector"; import { api } from "@/shared/lib/ky/connector";
const getOauthProviderList = async () => { const getOauthProviderList = async () => {
try { try {
const res = await api.get(`auth/providers`); const res = await api.get(`auth/providers`);
return res.json(); return res.json();
} catch (error) { } catch (error) {
throw new Error("Failed to fetch OAuth providers", { cause: error }); throw new Error("Failed to fetch OAuth providers", { cause: error });
} }
}; };
export default getOauthProviderList; export default getOauthProviderList;

View File

@ -1,36 +1,36 @@
"use server"; "use server";
import { api } from "@/shared/lib/ky/connector"; import { api } from "@/shared/lib/ky/connector";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { ResponseRequestOauthUrl } from "../types/responseRequestOauthUrl"; import { ResponseRequestOauthUrl } from "../types/responseRequestOauthUrl";
const requestOauthUrl = async (providerData: { const requestOauthUrl = async (providerData: {
name: string; name: string;
endpoint: string; endpoint: string;
}) => { }) => {
// Check if requestEndpoint is provided, if not throw an error // Check if requestEndpoint is provided, if not throw an error
if (!providerData.endpoint) if (!providerData.endpoint)
throw new Error("oAuth endpoint request not found"); throw new Error("oAuth endpoint request not found");
// Define a variable to hold the OAuth data // Define a variable to hold the OAuth data
let oauthData: Promise<ResponseRequestOauthUrl>; let oauthData: Promise<ResponseRequestOauthUrl>;
// Fetch OAuth data from the API // Fetch OAuth data from the API
try { try {
const response = await api.get(providerData.endpoint, { const response = await api.get(providerData.endpoint, {
searchParams: { searchParams: {
callback: `${ callback: `${
process.env.APP_DOMAIN process.env.APP_DOMAIN
}/auth/callback/${providerData.name.toLocaleLowerCase()}`, }/auth/callback/${providerData.name.toLocaleLowerCase()}`,
}, },
}); });
oauthData = response.json<ResponseRequestOauthUrl>(); oauthData = response.json<ResponseRequestOauthUrl>();
} catch (error) { } catch (error) {
throw new Error(JSON.stringify(error)); throw new Error(JSON.stringify(error));
} }
// Redirect to the OAuth provider's authorization page // Redirect to the OAuth provider's authorization page
redirect((await oauthData).data); redirect((await oauthData).data);
}; };
export default requestOauthUrl; export default requestOauthUrl;

View File

@ -1,27 +1,27 @@
"use server"; "use server";
import { apiErrorHandler } from "@/shared/lib/ky/errorHandler"; import { apiErrorHandler } from "@/shared/lib/ky/errorHandler";
import { RegisterInputs } from "../ui/components/ProvisionInput"; import { RegisterInputs } from "../ui/components/ProvisionInput";
import { ServerRequestCallback } from "@/shared/types/ServerRequestCallback"; import { ServerRequestCallback } from "@/shared/types/ServerRequestCallback";
export const submitRegisterForm = async ( export const submitRegisterForm = async (
data: RegisterInputs data: RegisterInputs
): Promise<ServerRequestCallback> => { ): Promise<ServerRequestCallback> => {
if (data.password !== data.confirmPassword) if (data.password !== data.confirmPassword)
return apiErrorHandler([], { return apiErrorHandler([], {
success: false, success: false,
status: 400, status: 400,
text: { message: "Password and Confirm Password do not match" }, text: { message: "Password and Confirm Password do not match" },
}); });
try { try {
await new Promise((resolve) => setTimeout(resolve, 3000)); await new Promise((resolve) => setTimeout(resolve, 3000));
return { return {
success: true, success: true,
status: 200, status: 200,
text: { message: "Registration successful" }, text: { message: "Registration successful" },
}; };
} catch (error) { } catch (error) {
return apiErrorHandler(error); return apiErrorHandler(error);
} }
}; };

View File

@ -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<typeof registerFormSchema>;

View File

@ -1,53 +1,53 @@
"use client"; "use client";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Login from "@/features/auth/ui/cards/Login"; import Login from "@/features/auth/ui/cards/Login";
import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup";
import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed";
const LoginPage = () => { const LoginPage = () => {
/** /**
* Create a lit component that will be used in popp, consisting of 3 component flows: * 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. * 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. * 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. * 3. If it fails, stop the authentication process and display the warning component, then return the user to the homepage.
*/ */
const componentFlowList = { const componentFlowList = {
securityCheckup: <SecurityCheckup />, securityCheckup: <SecurityCheckup />,
securityCheckupFailed: <SecurityCheckupFailed />, securityCheckupFailed: <SecurityCheckupFailed />,
SecurityCheckupSuccessed: <Login />, SecurityCheckupSuccessed: <Login />,
}; };
// State to set the current page component // State to set the current page component
const [componentFlow, setComponentFlow] = useState( const [componentFlow, setComponentFlow] = useState(
componentFlowList.securityCheckup componentFlowList.securityCheckup
); );
useEffect(() => { useEffect(() => {
/** /**
* Check if the window has an opener (i.e., it was opened by another window) * 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 does, the security checkup has passed.
* If it doesn't, the security checkup has failed and user will be redirected to the homepage. * If it doesn't, the security checkup has failed and user will be redirected to the homepage.
*/ */
if (window.opener) { if (window.opener) {
setComponentFlow(componentFlowList.SecurityCheckupSuccessed); setComponentFlow(componentFlowList.SecurityCheckupSuccessed);
} else { } else {
setComponentFlow(componentFlowList.securityCheckupFailed); setComponentFlow(componentFlowList.securityCheckupFailed);
const timer = setTimeout(() => { const timer = setTimeout(() => {
redirect("/"); redirect("/");
}, 5000); }, 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, []); }, []);
return ( return (
<> <>
{/* show the current component flow */} {/* show the current component flow */}
<main>{componentFlow}</main> <main>{componentFlow}</main>
</> </>
); );
}; };
export default LoginPage; export default LoginPage;

View File

@ -1,51 +1,51 @@
"use client"; "use client";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import React, { JSX, useEffect, useState } from "react"; import React, { JSX, useEffect, useState } from "react";
import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup";
import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed";
import Signup from "../ui/cards/Signup"; import Signup from "../ui/cards/Signup";
const SignupPage = () => { const SignupPage = () => {
// State to set the current page component // State to set the current page component
const [componentFlow, setComponentFlow] = useState<JSX.Element>( const [componentFlow, setComponentFlow] = useState<JSX.Element>(
<SecurityCheckup /> <SecurityCheckup />
); );
/** /**
* Create a lit component that will be used in popp, consisting of 3 component flows: * 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. * 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. * 2. If it fails, stop the authentication process and display the warning component, then return the user to the homepage.
*/ */
const componentFlowList = { const componentFlowList = {
securityCheckupFailed: <SecurityCheckupFailed />, securityCheckupFailed: <SecurityCheckupFailed />,
SecurityCheckupSuccessed: <Signup changeCurrentPage={setComponentFlow} />, SecurityCheckupSuccessed: <Signup changeCurrentPage={setComponentFlow} />,
}; };
useEffect(() => { useEffect(() => {
/** /**
* Check if the window has an opener (i.e., it was opened by another window) * 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 does, the security checkup has passed.
* If it doesn't, the security checkup has failed and user will be redirected to the homepage. * If it doesn't, the security checkup has failed and user will be redirected to the homepage.
*/ */
if (window.opener) { if (window.opener) {
setComponentFlow(componentFlowList.SecurityCheckupSuccessed); setComponentFlow(componentFlowList.SecurityCheckupSuccessed);
} else { } else {
setComponentFlow(componentFlowList.securityCheckupFailed); setComponentFlow(componentFlowList.securityCheckupFailed);
const timer = setTimeout(() => { const timer = setTimeout(() => {
redirect("/"); redirect("/");
}, 5000); }, 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, []); }, []);
return ( return (
<> <>
{/* show the current component flow */} {/* show the current component flow */}
<main>{componentFlow}</main> <main>{componentFlow}</main>
</> </>
); );
}; };
export default SignupPage; export default SignupPage;

View File

@ -1,8 +1,8 @@
export interface OauthProviders { export interface OauthProviders {
name: string; name: string;
icon: string; icon: string;
req_endpoint: string; req_endpoint: string;
client_id: string | undefined; client_id: string | undefined;
client_secret: string | undefined; client_secret: string | undefined;
client_callback: string; client_callback: string;
} }

View File

@ -1,5 +1,5 @@
export interface ResponseRequestOauthUrl { export interface ResponseRequestOauthUrl {
data: string; data: string;
message: string; message: string;
status: string; status: string;
} }

View File

@ -1,42 +1,42 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { Divider, Link } from "@heroui/react"; import { Divider, Link } from "@heroui/react";
import { routes } from "@/shared/config/routes"; import { routes } from "@/shared/config/routes";
import EmailInput from "../components/EmailInput"; import EmailInput from "../components/EmailInput";
import OAuthProviders from "../components/OAuthProviders"; import OAuthProviders from "../components/OAuthProviders";
const Login = () => { const Login = () => {
return ( return (
<div className="pt-12 max-w-[480px] mx-auto"> <div className="pt-12 max-w-[480px] mx-auto">
<div className="text-3xl text-center">Welcome back</div> <div className="text-3xl text-center">Welcome back</div>
{/* Email form */} {/* Email form */}
<div className="mt-6 px-3"> <div className="mt-6 px-3">
<EmailInput /> <EmailInput />
</div> </div>
{/* Sign up link */} {/* Sign up link */}
<p className="text-center text-neutral-300 text-sm font-light mt-5"> <p className="text-center text-neutral-300 text-sm font-light mt-5">
Don't have an account?{" "} Don't have an account?{" "}
<Link className="text-sm font-medium" href={routes.signup}> <Link className="text-sm font-medium" href={routes.signup}>
Sign Up Sign Up
</Link> </Link>
</p> </p>
{/* Divider between email form and third-party login options */} {/* Divider between email form and third-party login options */}
<div className="flex w-full items-center mt-6 px-10"> <div className="flex w-full items-center mt-6 px-10">
<Divider className="flex-1" /> <Divider className="flex-1" />
<span className="px-2 text-neutral-500 text-sm">or</span> <span className="px-2 text-neutral-500 text-sm">or</span>
<Divider className="flex-1" /> <Divider className="flex-1" />
</div> </div>
{/* Buttons for third-party login options */} {/* Buttons for third-party login options */}
<div className="mt-6 px-4"> <div className="mt-6 px-4">
<OAuthProviders /> <OAuthProviders />
</div> </div>
</div> </div>
); );
}; };
export default Login; export default Login;

View File

@ -1,22 +1,22 @@
"use client"; "use client";
import React from "react"; import React from "react";
import ProvisionInput from "../components/ProvisionInput"; import ProvisionInput from "../components/ProvisionInput";
type Props = { type Props = {
fullName: string; fullName: string;
}; };
const Provision = ({ fullName }: Props) => { const Provision = ({ fullName }: Props) => {
return ( return (
<div className="pt-12 max-w-[480px] mx-auto"> <div className="pt-12 max-w-[480px] mx-auto">
<div className="text-3xl text-center">Hey, {fullName.split(" ")[0]}</div> <div className="text-3xl text-center">Hey, {fullName.split(" ")[0]}</div>
<p className="text-sm text-center font-light text-neutral-300 mt-2"> <p className="text-sm text-center font-light text-neutral-300 mt-2">
Just a few more steps to join the fun! Just a few more steps to join the fun!
</p> </p>
<ProvisionInput fullname={fullName} /> <ProvisionInput fullname={fullName} />
</div> </div>
); );
}; };
export default Provision; export default Provision;

View File

@ -1,46 +1,46 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { Divider, Link } from "@heroui/react"; import { Divider, Link } from "@heroui/react";
import { routes } from "@/shared/config/routes"; import { routes } from "@/shared/config/routes";
import OAuthProviders from "../components/OAuthProviders"; import OAuthProviders from "../components/OAuthProviders";
import FullNameInput from "../components/FullNameInput"; import FullNameInput from "../components/FullNameInput";
type Props = { type Props = {
changeCurrentPage: React.Dispatch<React.SetStateAction<React.JSX.Element>>; changeCurrentPage: React.Dispatch<React.SetStateAction<React.JSX.Element>>;
}; };
const Signup = ({ changeCurrentPage }: Props) => { const Signup = ({ changeCurrentPage }: Props) => {
return ( return (
<div className="pt-12 max-w-[480px] mx-auto"> <div className="pt-12 max-w-[480px] mx-auto">
<div className="text-3xl text-center">Create an account</div> <div className="text-3xl text-center">Create an account</div>
{/* Email form */} {/* Email form */}
<div className="mt-6 px-3"> <div className="mt-6 px-3">
<FullNameInput changeCurrentPage={changeCurrentPage} /> <FullNameInput changeCurrentPage={changeCurrentPage} />
</div> </div>
{/* Sign up link */} {/* Sign up link */}
<p className="text-center text-neutral-300 text-sm font-light mt-5"> <p className="text-center text-neutral-300 text-sm font-light mt-5">
Already have an account?{" "} Already have an account?{" "}
<Link className="text-sm font-medium" href={routes.login}> <Link className="text-sm font-medium" href={routes.login}>
Log in Log in
</Link> </Link>
</p> </p>
{/* Divider between email form and third-party login options */} {/* Divider between email form and third-party login options */}
<div className="flex w-full items-center mt-6 px-10"> <div className="flex w-full items-center mt-6 px-10">
<Divider className="flex-1" /> <Divider className="flex-1" />
<span className="px-2 text-neutral-500 text-sm">or</span> <span className="px-2 text-neutral-500 text-sm">or</span>
<Divider className="flex-1" /> <Divider className="flex-1" />
</div> </div>
{/* Buttons for third-party login options */} {/* Buttons for third-party login options */}
<div className="mt-6 px-4"> <div className="mt-6 px-4">
<OAuthProviders /> <OAuthProviders />
</div> </div>
</div> </div>
); );
}; };
export default Signup; export default Signup;

View File

@ -1,26 +1,26 @@
"use client"; "use client";
import { Button, Input } from "@heroui/react"; import { Button, Input } from "@heroui/react";
import React from "react"; import React from "react";
const EmailInput = () => { const EmailInput = () => {
return ( return (
<> <>
<Input <Input
className="w-full " className="w-full "
label="Email" label="Email"
type="email" type="email"
variant="bordered" variant="bordered"
classNames={{ classNames={{
input: "text-md font-light pt-4", input: "text-md font-light pt-4",
inputWrapper: "flex gap-10", inputWrapper: "flex gap-10",
}} }}
/> />
<Button className="mt-2 w-full" color="primary"> <Button className="mt-2 w-full" color="primary">
Continue Continue
</Button> </Button>
</> </>
); );
}; };
export default EmailInput; export default EmailInput;

View File

@ -1,38 +1,38 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Input } from "@heroui/react"; import { Button, Input } from "@heroui/react";
import Provision from "../cards/Provision"; import Provision from "../cards/Provision";
type Props = { type Props = {
changeCurrentPage: React.Dispatch<React.SetStateAction<React.JSX.Element>>; changeCurrentPage: React.Dispatch<React.SetStateAction<React.JSX.Element>>;
}; };
const FullNameInput = ({ changeCurrentPage }: Props) => { const FullNameInput = ({ changeCurrentPage }: Props) => {
const [fullName, setFullName] = useState(""); const [fullName, setFullName] = useState("");
return ( return (
<> <>
<Input <Input
className="w-full " className="w-full "
label="Full Name" label="Full Name"
type="name" type="name"
variant="bordered" variant="bordered"
onChange={(e) => setFullName(e.target.value)} onChange={(e) => setFullName(e.target.value)}
classNames={{ classNames={{
input: "text-md font-light pt-4", input: "text-md font-light pt-4",
inputWrapper: "flex gap-10", inputWrapper: "flex gap-10",
}} }}
/> />
<Button <Button
onPress={() => changeCurrentPage(<Provision fullName={fullName} />)} onPress={() => changeCurrentPage(<Provision fullName={fullName} />)}
className="mt-2 w-full" className="mt-2 w-full"
color="primary" color="primary"
> >
Continue Continue
</Button> </Button>
</> </>
); );
}; };
export default FullNameInput; export default FullNameInput;

View File

@ -1,85 +1,85 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { OauthProviders } from "../../types/oauthProvidersList"; import { OauthProviders } from "../../types/oauthProvidersList";
import { ResponseRequestOauthUrl } from "../../types/responseRequestOauthUrl"; import { ResponseRequestOauthUrl } from "../../types/responseRequestOauthUrl";
import { Button } from "@heroui/react"; import { Button } from "@heroui/react";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import getOauthProviderList from "../../lib/getOauthProviderList"; import getOauthProviderList from "../../lib/getOauthProviderList";
import requestOauthUrl from "../../lib/requestOauthUrl"; import requestOauthUrl from "../../lib/requestOauthUrl";
const OAuthProviders = () => { const OAuthProviders = () => {
// Set initial state for OAuth providers list // Set initial state for OAuth providers list
const [oauthProvidersList, setOauthProvidersList] = useState< const [oauthProvidersList, setOauthProvidersList] = useState<
OauthProviders[] OauthProviders[]
>([]); >([]);
/** /**
* Fetch the list of OAuth providers from backend API * Fetch the list of OAuth providers from backend API
* and update the state if OAuth providers list is available * and update the state if OAuth providers list is available
*/ */
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const res = (await getOauthProviderList()) as OauthProviders[]; const res = (await getOauthProviderList()) as OauthProviders[];
setOauthProvidersList(res); setOauthProvidersList(res);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
})(); })();
}, []); }, []);
const [loadingButton, setLoadingButton] = useState(false); const [loadingButton, setLoadingButton] = useState(false);
/** /**
* Start the authentication process using oAuth by sending the endpoint URL to the backend for processing. * 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 * @param providerRequestEndpoint The request endpoint for the OAuth provider
*/ */
const startOauthProcess = async (providerData: { const startOauthProcess = async (providerData: {
name: string; name: string;
endpoint: string; endpoint: string;
}) => { }) => {
try { try {
setLoadingButton(true); setLoadingButton(true);
(await requestOauthUrl(providerData)) as ResponseRequestOauthUrl; (await requestOauthUrl(providerData)) as ResponseRequestOauthUrl;
} catch (err) { } catch (err) {
setLoadingButton(false); setLoadingButton(false);
console.error(err); console.error(err);
} }
}; };
return ( return (
<div className="w-full flex flex-col gap-2 mt-4"> <div className="w-full flex flex-col gap-2 mt-4">
{/* Render OAuth provider buttons */} {/* Render OAuth provider buttons */}
{oauthProvidersList.length > 0 ? ( {oauthProvidersList.length > 0 ? (
oauthProvidersList.map((provider, index) => { oauthProvidersList.map((provider, index) => {
return ( return (
<Button <Button
key={index} key={index}
className="w-full hover:bg-neutral-800" className="w-full hover:bg-neutral-800"
variant="bordered" variant="bordered"
startContent={<Icon className="w-4 h-4" icon={provider.icon} />} startContent={<Icon className="w-4 h-4" icon={provider.icon} />}
onPress={() => onPress={() =>
startOauthProcess({ startOauthProcess({
name: provider.name, name: provider.name,
endpoint: provider.req_endpoint, endpoint: provider.req_endpoint,
}) })
} }
isLoading={loadingButton} isLoading={loadingButton}
> >
Continue with {provider.name} Continue with {provider.name}
</Button> </Button>
); );
}) })
) : ( ) : (
<Button className="w-full" variant="ghost" isDisabled> <Button className="w-full" variant="ghost" isDisabled>
No login options available via third-party providers No login options available via third-party providers
</Button> </Button>
)} )}
</div> </div>
); );
}; };
export default OAuthProviders; export default OAuthProviders;

View File

@ -1,103 +1,118 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { addToast, Button, Form, Input } from "@heroui/react"; import { addToast, Button, Form, Input } from "@heroui/react";
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import { submitRegisterForm } from "../../lib/submitRegisterForm"; import { submitRegisterForm } from "../../lib/submitRegisterForm";
import { zodResolver } from "@hookform/resolvers/zod";
type Props = { import { registerFormSchema } from "../../models/registerForm.schema";
fullname: string;
}; type Props = {
fullname: string;
export type RegisterInputs = { };
fullname: string;
email: string; export type RegisterInputs = {
password: string; fullname: string;
confirmPassword: string; email: string;
}; password: string;
confirmPassword: string;
const ProvisionInput = ({ fullname }: Props) => { };
const { register, handleSubmit, setValue } = useForm<RegisterInputs>();
setValue("fullname", fullname); const ProvisionInput = ({ fullname }: Props) => {
const {
const [submitStatus, setSubmitStatus] = useState(false); register,
const onSubmit: SubmitHandler<RegisterInputs> = async (data) => { handleSubmit,
setSubmitStatus(true); setValue,
formState: { errors },
try { } = useForm<RegisterInputs>({
const returnData = await submitRegisterForm(data); resolver: zodResolver(registerFormSchema),
if (!returnData.success) { });
setSubmitStatus(false); setValue("fullname", fullname);
addToast({
color: "danger", const [submitStatus, setSubmitStatus] = useState(false);
title: "😬 Oops, something went wrong!", const onSubmit: SubmitHandler<RegisterInputs> = async (data) => {
description: returnData.text.message, setSubmitStatus(true);
});
} else { try {
setSubmitStatus(false); const returnData = await submitRegisterForm(data);
addToast({ if (!returnData.success) {
color: "success", setSubmitStatus(false);
title: "OKKE!", addToast({
description: returnData.text.message, color: "danger",
}); title: "😬 Oops, something went wrong!",
} description: returnData.text.message,
} catch (error) { });
setSubmitStatus(false); } else {
addToast({ setSubmitStatus(false);
color: "danger", addToast({
title: "😬 Oops, something went wrong!", color: "success",
description: "Internal server error", title: "OKKE!",
}); description: returnData.text.message,
} });
}; }
} catch (error) {
return ( setSubmitStatus(false);
<div className="mt-6 px-3"> addToast({
<Form className="flex flex-col gap-1.5" onSubmit={handleSubmit(onSubmit)}> color: "danger",
<Input title: "😬 Oops, something went wrong!",
{...register("email")} description: "Connection to server lost",
className="w-full " });
label="Email" }
type="email" };
variant="bordered"
classNames={{ return (
input: "text-md font-light pt-4", <div className="mt-6 px-3">
inputWrapper: "flex gap-10", <Form className="flex flex-col gap-1.5" onSubmit={handleSubmit(onSubmit)}>
}} <Input
/> {...register("email")}
<Input className="w-full "
{...register("password")} label="Email"
className="w-full " type="email"
label="Password" variant="bordered"
type="password" isInvalid={errors.email ? true : false}
variant="bordered" errorMessage={errors.email?.message}
classNames={{ classNames={{
input: "text-md font-light pt-4", input: "text-md font-light pt-4",
inputWrapper: "flex gap-10", inputWrapper: "flex gap-10",
}} }}
/> />
<Input <Input
{...register("confirmPassword")} {...register("password")}
className="w-full " className="w-full "
label="Confirm Password" label="Password"
type="password" type="password"
variant="bordered" variant="bordered"
classNames={{ isInvalid={errors.password ? true : false}
input: "text-md font-light pt-4", errorMessage={errors.password?.message}
inputWrapper: "flex gap-10", classNames={{
}} input: "text-md font-light pt-4",
/> inputWrapper: "flex gap-10",
<Button }}
type="submit" />
className="mt-1.5 w-full" <Input
color="primary" {...register("confirmPassword")}
isLoading={submitStatus} className="w-full "
> label="Confirm Password"
Continue type="password"
</Button> variant="bordered"
</Form> isInvalid={errors.confirmPassword ? true : false}
</div> errorMessage={errors.confirmPassword?.message}
); classNames={{
}; input: "text-md font-light pt-4",
inputWrapper: "flex gap-10",
export default ProvisionInput; }}
/>
<Button
type="submit"
className="mt-1.5 w-full"
color="primary"
isLoading={submitStatus}
>
Continue
</Button>
</Form>
</div>
);
};
export default ProvisionInput;

View File

@ -1,83 +1,83 @@
"use server"; "use server";
import { api } from "@/shared/lib/ky/connector"; import { api } from "@/shared/lib/ky/connector";
import { apiErrorHandler } from "@/shared/lib/ky/errorHandler"; import { apiErrorHandler } from "@/shared/lib/ky/errorHandler";
import { ServerRequestCallback } from "@/shared/types/serverRequestCallback"; import { ServerRequestCallback } from "@/shared/types/serverRequestCallback";
/** /**
* @function SendCallbackToServer * @function SendCallbackToServer
* @description Proxies OAuth callback requests from the frontend to the main backend system. * @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. * 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. * Handles the forwarding of OAuth provider callback data for authentication processing.
* *
* @param {string} data - The OAuth callback data received from the provider, typically containing * @param {string} data - The OAuth callback data received from the provider, typically containing
* query parameters such as authorization code, user consent, scopes, state, * query parameters such as authorization code, user consent, scopes, state,
* and other OAuth-specific information. Usually obtained from * and other OAuth-specific information. Usually obtained from
* `window.location.search` in browser environments. * `window.location.search` in browser environments.
* @param {string} provider - The name of the OAuth provider/service (e.g., "google", "github", * @param {string} provider - The name of the OAuth provider/service (e.g., "google", "github",
* "facebook"). Used to construct the appropriate backend API endpoint. * "facebook"). Used to construct the appropriate backend API endpoint.
* *
* @returns {Promise<Object>} The response data from the backend server after processing the * @returns {Promise<Object>} The response data from the backend server after processing the
* OAuth callback. Typically contains authentication tokens, user * OAuth callback. Typically contains authentication tokens, user
* information, or session data. * information, or session data.
* *
* @throws {Error} If the network request fails or the backend returns an error response. * @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 required environment variable APP_DOMAIN is not defined.
* @throws {Error} If the provided parameters are invalid or missing. * @throws {Error} If the provided parameters are invalid or missing.
* *
* @example * @example
* // Handling OAuth callback in a React component * // Handling OAuth callback in a React component
* useEffect(() => { * useEffect(() => {
* const handleOAuthCallback = async () => { * const handleOAuthCallback = async () => {
* try { * try {
* const result = await SendCallbackToServer(window.location.search, "google"); * const result = await SendCallbackToServer(window.location.search, "google");
* // Handle successful authentication (e.g., store tokens, redirect user) * // Handle successful authentication (e.g., store tokens, redirect user)
* console.log("Authentication successful:", result); * console.log("Authentication successful:", result);
* } catch (error) { * } catch (error) {
* // Handle authentication errors * // Handle authentication errors
* console.error("Authentication failed:", error); * console.error("Authentication failed:", error);
* } * }
* }; * };
* *
* handleOAuthCallback(); * handleOAuthCallback();
* }, []); * }, []);
* *
* @example * @example
* // Usage with different providers * // Usage with different providers
* await SendCallbackToServer(window.location.search, "github"); * await SendCallbackToServer(window.location.search, "github");
* await SendCallbackToServer(window.location.search, "facebook"); * await SendCallbackToServer(window.location.search, "facebook");
* await SendCallbackToServer(window.location.search, "microsoft"); * await SendCallbackToServer(window.location.search, "microsoft");
* *
* @remarks * @remarks
* - This function is specifically designed for OAuth callback handling in a Next.js frontend * - This function is specifically designed for OAuth callback handling in a Next.js frontend
* acting as a proxy to an Elysia backend. * acting as a proxy to an Elysia backend.
* - The `data` parameter should include the complete query string from the OAuth redirect. * - 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. * - The callback URI is automatically constructed using the APP_DOMAIN environment variable.
* - Ensure APP_DOMAIN is properly configured in your environment variables. * - Ensure APP_DOMAIN is properly configured in your environment variables.
*/ */
export const SendCallbackToServer = async ( export const SendCallbackToServer = async (
data: string, data: string,
provider: string provider: string
): Promise<ServerRequestCallback> => { ): Promise<ServerRequestCallback> => {
// Construct the backend and frontend handler URLs // Construct the backend and frontend handler URLs
const backendHandlerUrl = `auth/${provider}/callback/`; const backendHandlerUrl = `auth/${provider}/callback/`;
const frontendHandlerUrl = `${process.env const frontendHandlerUrl = `${process.env
.APP_DOMAIN!}/auth/callback/${provider}`; .APP_DOMAIN!}/auth/callback/${provider}`;
try { try {
// Forward the OAuth callback data to the backend for processing // Forward the OAuth callback data to the backend for processing
const response = await api.get( const response = await api.get(
`${backendHandlerUrl}${data}&callbackURI=${frontendHandlerUrl}` `${backendHandlerUrl}${data}&callbackURI=${frontendHandlerUrl}`
); );
// Parse the JSON response from the backend and return the result // Parse the JSON response from the backend and return the result
const result = await response.json(); const result = await response.json();
return { return {
success: true, success: true,
status: response.status, status: response.status,
text: { message: "Callback processed successfully" }, text: { message: "Callback processed successfully" },
data: result, data: result,
}; };
} catch (error) { } catch (error) {
return apiErrorHandler(error); return apiErrorHandler(error);
} }
}; };

View File

@ -1,56 +1,56 @@
"use client"; "use client";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup"; import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup";
import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed"; import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed";
import LoadingProcess from "../ui/LoadingProcess"; import LoadingProcess from "../ui/LoadingProcess";
const OauthCallbackHandler = () => { const OauthCallbackHandler = () => {
/** /**
* Create a lit component that will be used in popp, consisting of 3 component flows: * 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. * 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. * 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. * 3. If it fails, stop the authentication process and display the warning component, then return the user to the homepage.
*/ */
const componentFlowList = { const componentFlowList = {
securityCheckup: <SecurityCheckup />, securityCheckup: <SecurityCheckup />,
securityCheckupFailed: <SecurityCheckupFailed />, securityCheckupFailed: <SecurityCheckupFailed />,
proceedCallback: <LoadingProcess />, proceedCallback: <LoadingProcess />,
}; };
// State to set the current page component // State to set the current page component
const [componentFlow, setComponentFlow] = useState( const [componentFlow, setComponentFlow] = useState(
componentFlowList.securityCheckup componentFlowList.securityCheckup
); );
useEffect(() => { useEffect(() => {
// Prevent opening devtools while in authentication page // Prevent opening devtools while in authentication page
// disableDevtool(); // disableDevtool();
/** /**
* Check if the window has an opener (i.e., it was opened by another window) * 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 does, the security checkup has passed.
* If it doesn't, the security checkup has failed and user will be redirected to the homepage. * If it doesn't, the security checkup has failed and user will be redirected to the homepage.
*/ */
if (window.opener) { if (window.opener) {
setComponentFlow(componentFlowList.proceedCallback); setComponentFlow(componentFlowList.proceedCallback);
} else { } else {
setComponentFlow(componentFlowList.securityCheckupFailed); setComponentFlow(componentFlowList.securityCheckupFailed);
const timer = setTimeout(() => { const timer = setTimeout(() => {
redirect("/"); redirect("/");
}, 5000); }, 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, []); }, []);
return ( return (
<> <>
{/* show the current component flow */} {/* show the current component flow */}
<main>{componentFlow}</main> <main>{componentFlow}</main>
</> </>
); );
}; };
export default OauthCallbackHandler; export default OauthCallbackHandler;

View File

@ -1,9 +1,9 @@
export interface ParamProps { export interface ParamProps {
params: { provider: string[] }; params: { provider: string[] };
searchParams: searchParams:
| string | string
| string[][] | string[][]
| Record<string, string> | Record<string, string>
| URLSearchParams | URLSearchParams
| undefined; | undefined;
} }

View File

@ -1,73 +1,73 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { addToast, Button, CircularProgress, Link } from "@heroui/react"; import { addToast, Button, CircularProgress, Link } from "@heroui/react";
import { SendCallbackToServer } from "../lib/sendCallbackToServer"; import { SendCallbackToServer } from "../lib/sendCallbackToServer";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useRunOnce } from "@/shared/hooks/useRunOnce"; import { useRunOnce } from "@/shared/hooks/useRunOnce";
import { routes } from "@/shared/config/routes"; import { routes } from "@/shared/config/routes";
const LoadingProcess = () => { const LoadingProcess = () => {
// Access the URL parameters // Access the URL parameters
const params = useParams(); const params = useParams();
// Forward the callback response to the backend server // Forward the callback response to the backend server
useRunOnce("forwardCallbackResponseToBackend", async () => { useRunOnce("forwardCallbackResponseToBackend", async () => {
try { try {
const response = await SendCallbackToServer( const response = await SendCallbackToServer(
window.location.search, window.location.search,
params.provider as string params.provider as string
); );
if (response.success) { if (response.success) {
window.close(); window.close();
} else { } else {
addToast({ addToast({
title: "😬 Oops, there's a problem!", title: "😬 Oops, there's a problem!",
description: response.text.message, description: response.text.message,
color: "danger", color: "danger",
timeout: 0, timeout: 0,
endContent: ( endContent: (
<Button <Button
size="sm" size="sm"
variant="flat" variant="flat"
onPress={() => (window.location.href = routes.login)} onPress={() => (window.location.href = routes.login)}
> >
Try again Try again
</Button> </Button>
), ),
}); });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
addToast({ addToast({
title: "😵‍💫 Oops, lost connection!", title: "😵‍💫 Oops, lost connection!",
description: "Check your internet and try again", description: "Check your internet and try again",
color: "danger", color: "danger",
timeout: 0, timeout: 0,
endContent: ( endContent: (
<Button <Button
size="sm" size="sm"
variant="flat" variant="flat"
onPress={() => (window.location.href = routes.login)} onPress={() => (window.location.href = routes.login)}
> >
Reload Reload
</Button> </Button>
), ),
}); });
} }
}); });
return ( return (
<div className="w-full flex flex-col items-center text-center mt-[26vh]"> <div className="w-full flex flex-col items-center text-center mt-[26vh]">
<CircularProgress aria-label="Loading..." size="lg" /> <CircularProgress aria-label="Loading..." size="lg" />
<div className="mt-4"> <div className="mt-4">
<h1 className="text-lg text-neutral-200">Please wait...</h1> <h1 className="text-lg text-neutral-200">Please wait...</h1>
<p className="text-sm text-neutral-400"> <p className="text-sm text-neutral-400">
Your request is being processed Your request is being processed
</p> </p>
</div> </div>
</div> </div>
); );
}; };
export default LoadingProcess; export default LoadingProcess;

View File

@ -1,42 +1,42 @@
import {withSentryConfig} from "@sentry/nextjs"; import {withSentryConfig} from "@sentry/nextjs";
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
env: { env: {
NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN, NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN,
} }
}; };
export default withSentryConfig(nextConfig, { export default withSentryConfig(nextConfig, {
// For all available options, see: // For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options // https://www.npmjs.com/package/@sentry/webpack-plugin#options
org: process.env.NEXT_PUBLIC_SENTRY_ORG || "", org: process.env.NEXT_PUBLIC_SENTRY_ORG || "",
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT || "", project: process.env.NEXT_PUBLIC_SENTRY_PROJECT || "",
sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL || "", sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL || "",
// Only print logs for uploading source maps in CI // Only print logs for uploading source maps in CI
silent: !process.env.CI, silent: !process.env.CI,
// For all available options, see: // For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time) // Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true, widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. // 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. // 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- // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail. // side errors will fail.
tunnelRoute: "/monitoring", tunnelRoute: "/monitoring",
// Automatically tree-shake Sentry logger statements to reduce bundle size // Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true, disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information: // See the following for more information:
// https://docs.sentry.io/product/crons/ // https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs // https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true automaticVercelMonitors: true
}); });

View File

@ -1,6 +1,6 @@
const config = { const config = {
plugins: { plugins: {
"@tailwindcss/postcss": {}, "@tailwindcss/postcss": {},
}, },
}; };
export default config; export default config;

View File

@ -1,32 +1,32 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { HeroUIProvider, ToastProvider } from "@heroui/react"; import { HeroUIProvider, ToastProvider } from "@heroui/react";
import { ThemeProvider as NextThemesProvider } from "next-themes"; import { ThemeProvider as NextThemesProvider } from "next-themes";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
const HeroUIWrapper = ({ const HeroUIWrapper = ({
children, children,
}: Readonly<{ children: React.ReactNode }>) => { }: Readonly<{ children: React.ReactNode }>) => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
const router = useRouter(); const router = useRouter();
return ( return (
<HeroUIProvider navigate={router.push}> <HeroUIProvider navigate={router.push}>
<ToastProvider /> <ToastProvider />
{mounted ? ( {mounted ? (
<NextThemesProvider attribute={"class"} defaultTheme="dark"> <NextThemesProvider attribute={"class"} defaultTheme="dark">
<main>{children}</main> <main>{children}</main>
</NextThemesProvider> </NextThemesProvider>
) : ( ) : (
<main className="dark text-foreground bg-background">{children}</main> <main className="dark text-foreground bg-background">{children}</main>
)} )}
</HeroUIProvider> </HeroUIProvider>
); );
}; };
export default HeroUIWrapper; export default HeroUIWrapper;

View File

@ -1,60 +1,60 @@
import React from "react"; import React from "react";
import localFont from "next/font/local"; import localFont from "next/font/local";
const Geist = localFont({ const Geist = localFont({
src: [ src: [
{ {
path: "../../fonts/Geist-Black.ttf", path: "../../fonts/Geist-Black.ttf",
weight: "900", weight: "900",
style: "normal", style: "normal",
}, },
{ {
path: "../../fonts/Geist-ExtraBold.ttf", path: "../../fonts/Geist-ExtraBold.ttf",
weight: "800", weight: "800",
style: "normal", style: "normal",
}, },
{ {
path: "../../fonts/Geist-Bold.ttf", path: "../../fonts/Geist-Bold.ttf",
weight: "700", weight: "700",
style: "normal", style: "normal",
}, },
{ {
path: "../../fonts/Geist-SemiBold.ttf", path: "../../fonts/Geist-SemiBold.ttf",
weight: "600", weight: "600",
style: "normal", style: "normal",
}, },
{ {
path: "../../fonts/Geist-Medium.ttf", path: "../../fonts/Geist-Medium.ttf",
weight: "500", weight: "500",
style: "normal", style: "normal",
}, },
{ {
path: "../../fonts/Geist-Regular.ttf", path: "../../fonts/Geist-Regular.ttf",
weight: "400", weight: "400",
style: "normal", style: "normal",
}, },
{ {
path: "../../fonts/Geist-Light.ttf", path: "../../fonts/Geist-Light.ttf",
weight: "300", weight: "300",
style: "normal", style: "normal",
}, },
{ {
path: "../../fonts/Geist-ExtraLight.ttf", path: "../../fonts/Geist-ExtraLight.ttf",
weight: "200", weight: "200",
style: "normal", style: "normal",
}, },
{ {
path: "../../fonts/Geist-Thin.ttf", path: "../../fonts/Geist-Thin.ttf",
weight: "100", weight: "100",
style: "normal", style: "normal",
}, },
], ],
}); });
const GeistFontProvider = ({ const GeistFontProvider = ({
children, children,
}: Readonly<{ children: React.ReactNode }>) => { }: Readonly<{ children: React.ReactNode }>) => {
return <div className={`${Geist.className}`}>{children}</div>; return <div className={`${Geist.className}`}>{children}</div>;
}; };
export default GeistFontProvider; export default GeistFontProvider;

View File

@ -1,49 +1,49 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
// These keys will not be cleared in the .env.example file // These keys will not be cleared in the .env.example file
const PRESERVED_KEYS = ["APP_NAME", "APP_ENV", "APP_PORT"]; const PRESERVED_KEYS = ["APP_NAME", "APP_ENV", "APP_PORT"];
/** /**
* Script to create or update the .env.example file based on the .env file. * 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. * It preserves certain keys and clears their values in the .env.example file.
*/ */
try { try {
const envPath = path.join(process.cwd(), ".env"); const envPath = path.join(process.cwd(), ".env");
const envExamplePath = path.join(process.cwd(), ".env.example"); const envExamplePath = path.join(process.cwd(), ".env.example");
if (!fs.existsSync(envPath)) { if (!fs.existsSync(envPath)) {
console.error(`.env file not found at ${envPath}`); console.error(`.env file not found at ${envPath}`);
process.exit(1); process.exit(1);
} }
const envContent = fs.readFileSync(envPath, "utf-8"); const envContent = fs.readFileSync(envPath, "utf-8");
const lines = envContent.split("\n"); const lines = envContent.split("\n");
const processedLines = lines.map((line) => { const processedLines = lines.map((line) => {
const trimmedLine = line.trim(); const trimmedLine = line.trim();
if (trimmedLine.startsWith("#") || trimmedLine === "") { if (trimmedLine.startsWith("#") || trimmedLine === "") {
return line; return line;
} }
const delimeterIndex = line.indexOf("="); const delimeterIndex = line.indexOf("=");
if (delimeterIndex === -1) { if (delimeterIndex === -1) {
return line; return line;
} }
const key = line.substring(0, delimeterIndex).trim(); const key = line.substring(0, delimeterIndex).trim();
const value = line.substring(delimeterIndex + 1).trim(); const value = line.substring(delimeterIndex + 1).trim();
if (PRESERVED_KEYS.includes(key)) { if (PRESERVED_KEYS.includes(key)) {
return `${key}=${value}`; return `${key}=${value}`;
} }
return `${key}=`; return `${key}=`;
}); });
fs.writeFileSync(envExamplePath, processedLines.join("\n")); fs.writeFileSync(envExamplePath, processedLines.join("\n"));
console.log("File .env.example berhasil diperbarui!"); console.log("File .env.example berhasil diperbarui!");
} catch (error) { } catch (error) {
console.error("Error while creating .env.example:", error); console.error("Error while creating .env.example:", error);
process.exit(1); process.exit(1);
} }

View File

@ -1,22 +1,22 @@
import { execSync } from "child_process"; import { execSync } from "child_process";
/* /*
This script pushes the current branch to multiple remotes in a Git repository. 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. 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! 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 const remotes = ["origin"]; // Add your remote names here, e.g., "origin", "vps", etc. if you have multiple remotes
// Start the push process // Start the push process
for (const remote of remotes) { for (const remote of remotes) {
console.log(`Pushing to ${remote}...`); console.log(`Pushing to ${remote}...`);
try { try {
execSync(`git push ${remote} main`, { stdio: "inherit" }); execSync(`git push ${remote} main`, { stdio: "inherit" });
} catch (err) { } catch (err) {
console.error(`❌ Failed to push to ${remote}`); console.error(`❌ Failed to push to ${remote}`);
} }
} }
// All remotes processed // All remotes processed
console.log("✅ All remotes processed."); console.log("✅ All remotes processed.");

View File

@ -1,17 +1,17 @@
import React from "react"; import React from "react";
const SecurityCheckup = () => { const SecurityCheckup = () => {
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="max-w-[60vh]"> <div className="max-w-[60vh]">
<h1 className="mt-[20vh] text-2xl text-center">Please wait...</h1> <h1 className="mt-[20vh] text-2xl text-center">Please wait...</h1>
<p className="mt-4 text-sm text-center text-neutral-400"> <p className="mt-4 text-sm text-center text-neutral-400">
We want to ensure a secure authentication environment before We want to ensure a secure authentication environment before
proceeding for your safety. proceeding for your safety.
</p> </p>
</div> </div>
</div> </div>
); );
}; };
export default SecurityCheckup; export default SecurityCheckup;

View File

@ -1,19 +1,19 @@
import React from "react"; import React from "react";
const SecurityCheckupFailed = () => { const SecurityCheckupFailed = () => {
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="max-w-[60vh]"> <div className="max-w-[60vh]">
<h1 className="mt-[20vh] text-2xl text-center text-red-400"> <h1 className="mt-[20vh] text-2xl text-center text-red-400">
Your browser is not secure Your browser is not secure
</h1> </h1>
<p className="mt-4 text-sm text-center text-neutral-400"> <p className="mt-4 text-sm text-center text-neutral-400">
Sorry, we had to stop the authentication process and return you to the Sorry, we had to stop the authentication process and return you to the
home page because your browser environment is not secure. home page because your browser environment is not secure.
</p> </p>
</div> </div>
</div> </div>
); );
}; };
export default SecurityCheckupFailed; export default SecurityCheckupFailed;

View File

@ -1,29 +1,29 @@
type BuildMeta = { type BuildMeta = {
title?: string; title?: string;
description?: string; description?: string;
image?: string; image?: string;
}; };
const appName = process.env.APP_NAME; const appName = process.env.APP_NAME;
export const defaultMeta = { export const defaultMeta = {
title: appName || "Unknown App", title: appName || "Unknown App",
description: "Interactive community", description: "Interactive community",
}; };
export const buildMeta = ({ title, description, image }: BuildMeta) => { export const buildMeta = ({ title, description, image }: BuildMeta) => {
return { return {
title: title ? `${title} - ${appName}` : defaultMeta.title, title: title ? `${title} - ${appName}` : defaultMeta.title,
description: description || defaultMeta.description, description: description || defaultMeta.description,
openGraph: { openGraph: {
title, title,
description, description,
images: image ? [image] : ["/default-og.png"], images: image ? [image] : ["/default-og.png"],
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title, title,
description, description,
images: image ? [image] : ["/default-og.png"], images: image ? [image] : ["/default-og.png"],
}, },
}; };
}; };

View File

@ -1,9 +1,9 @@
export const routes = { export const routes = {
home: "/", home: "/",
login: "/login", login: "/login",
signup: "/signup", signup: "/signup",
explore: "/explore", explore: "/explore",
trending: "/trending", trending: "/trending",
genres: "/genres", genres: "/genres",
schedule: "/schedule", schedule: "/schedule",
}; };

View File

@ -1,21 +1,21 @@
export const API_BASE_URL = export const API_BASE_URL =
process.env.MAIN_BACKEND_API_URL ?? "http://localhost"; process.env.MAIN_BACKEND_API_URL ?? "http://localhost";
const apiFetch = async <T = unknown>( const apiFetch = async <T = unknown>(
path: string, path: string,
init?: RequestInit init?: RequestInit
): Promise<T> => { ): Promise<T> => {
const res = await fetch(`${API_BASE_URL}${path}`, { const res = await fetch(`${API_BASE_URL}${path}`, {
...init, ...init,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...init?.headers, ...init?.headers,
}, },
cache: "no-store", cache: "no-store",
}); });
if (!res.ok) throw new Error(await res.text()); if (!res.ok) throw new Error(await res.text());
return res.json(); return res.json();
}; };
export default apiFetch; export default apiFetch;

View File

@ -1,11 +1,11 @@
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export const delayButtonClick = ( export const delayButtonClick = (
router: ReturnType<typeof useRouter>, router: ReturnType<typeof useRouter>,
href: string, href: string,
timeout: number = 300 timeout: number = 300
) => { ) => {
setTimeout(() => { setTimeout(() => {
router.push(href); router.push(href);
}, timeout); }, timeout);
}; };

View File

@ -1,58 +1,58 @@
"use client"; "use client";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
/** /**
* @function useRunOnce * @function useRunOnce
* @description A custom React hook that ensures a function is executed only once * @description A custom React hook that ensures a function is executed only once
* across the entire application, even in React Strict Mode or during * across the entire application, even in React Strict Mode or during
* development hot reloads. Maintains a global registry to track * development hot reloads. Maintains a global registry to track
* execution status using a unique key. * execution status using a unique key.
* *
* Particularly useful for one-time initialization logic, analytics * Particularly useful for one-time initialization logic, analytics
* tracking, or any operation that should not be duplicated. * tracking, or any operation that should not be duplicated.
* *
* @param {string} key - A unique identifier for the process. Used to track execution * @param {string} key - A unique identifier for the process. Used to track execution
* across component instances and prevent naming collisions. * across component instances and prevent naming collisions.
* Should be descriptive and unique (e.g., "user_analytics_init"). * Should be descriptive and unique (e.g., "user_analytics_init").
* @param {function} fn - The function to be executed once. Should contain the logic * @param {function} fn - The function to be executed once. Should contain the logic
* that needs to run only a single time. * that needs to run only a single time.
* *
* @returns {void} * @returns {void}
* *
* @throws {Error} If the provided key is not a string or is empty. * @throws {Error} If the provided key is not a string or is empty.
* @throws {Error} If the provided function is not callable. * @throws {Error} If the provided function is not callable.
* *
* @example * @example
* // One-time asynchronous operation * // One-time asynchronous operation
* useRunOnce("async_operation", async () => { * useRunOnce("async_operation", async () => {
* await yourAsyncFunction(); * await yourAsyncFunction();
* }); * });
* *
* @example * @example
* // One-time synchronous operation * // One-time synchronous operation
* useRunOnce("sync_operation", () => { * useRunOnce("sync_operation", () => {
* yourFunction(); * yourFunction();
* }); * });
* *
* @remarks * @remarks
* - The hook uses a global registry, so the same key across different components * - The hook uses a global registry, so the same key across different components
* will prevent duplicate execution. * will prevent duplicate execution.
* - Safe to use in React Strict Mode and development environment with hot reload. * - 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 * - The function will not execute if another instance with the same key has
* already run in the application. * already run in the application.
*/ */
const registry = new Set<string>(); const registry = new Set<string>();
export function useRunOnce(key: string, fn: () => void) { export function useRunOnce(key: string, fn: () => void) {
const hasRun = useRef(false); const hasRun = useRef(false);
useEffect(() => { useEffect(() => {
if (hasRun.current) return; if (hasRun.current) return;
hasRun.current = true; hasRun.current = true;
if (registry.has(key)) return; if (registry.has(key)) return;
registry.add(key); registry.add(key);
fn(); fn();
}, [key, fn]); }, [key, fn]);
} }

View File

@ -1,12 +1,12 @@
"use server"; "use server";
import ky from "ky"; import ky from "ky";
export const api = ky.create({ export const api = ky.create({
prefixUrl: process.env.MAIN_BACKEND_API_URL, prefixUrl: process.env.MAIN_BACKEND_API_URL,
credentials: "include", credentials: "include",
headers: { headers: {
access_token: process.env.MAIN_BACKEND_API_KEY, access_token: process.env.MAIN_BACKEND_API_KEY,
}, },
retry: 0, retry: 0,
}); });

View File

@ -1,34 +1,34 @@
"use server"; "use server";
import { HTTPError } from "ky"; import { HTTPError } from "ky";
export type CallApiErrorHandler = { export type CallApiErrorHandler = {
success?: boolean; success?: boolean;
status?: number; status?: number;
text?: { message?: string }; text?: { message?: string };
}; };
export const apiErrorHandler = async ( export const apiErrorHandler = async (
error: unknown, error: unknown,
safeFail?: CallApiErrorHandler safeFail?: CallApiErrorHandler
) => { ) => {
if (error instanceof HTTPError) { if (error instanceof HTTPError) {
return { return {
success: false, success: false,
status: error.response.status, status: error.response.status,
text: await error.response.json(), text: await error.response.json(),
}; };
} }
if (safeFail) { if (safeFail) {
return { return {
success: safeFail.success || false, success: safeFail.success || false,
status: safeFail.status || 500, status: safeFail.status || 500,
text: { text: {
message: safeFail.text?.message || "An unexpected error occurred", message: safeFail.text?.message || "An unexpected error occurred",
}, },
}; };
} else { } else {
throw error; throw error;
} }
}; };

View File

@ -1,7 +1,7 @@
export type ServerRequestCallback = { export type ServerRequestCallback = {
success: boolean; success: boolean;
status: number; status: number;
text: { message: string }; text: { message: string };
data?: any; data?: any;
error?: unknown; error?: unknown;
}; };

View File

@ -1,37 +1,37 @@
"use client"; "use client";
import { routes } from "@/shared/config/routes"; import { routes } from "@/shared/config/routes";
import { Button, Link, NavbarItem } from "@heroui/react"; import { Button, Link, NavbarItem } from "@heroui/react";
import React from "react"; import React from "react";
const LoginAndSignup = () => { const LoginAndSignup = () => {
const openPopupWindow = (href: string) => { const openPopupWindow = (href: string) => {
window.open(href, "popup", "width=500,height=600"); window.open(href, "popup", "width=500,height=600");
}; };
return ( return (
<> <>
<NavbarItem className="hidden lg:flex"> <NavbarItem className="hidden lg:flex">
<Button <Button
color="primary" color="primary"
variant="light" variant="light"
className="font-medium" className="font-medium"
onPress={() => openPopupWindow(routes.login)} onPress={() => openPopupWindow(routes.login)}
> >
Login Login
</Button> </Button>
</NavbarItem> </NavbarItem>
<NavbarItem> <NavbarItem>
<Button <Button
color="primary" color="primary"
variant="solid" variant="solid"
radius="sm" radius="sm"
onPress={() => openPopupWindow(routes.signup)} onPress={() => openPopupWindow(routes.signup)}
> >
Sign Up Sign Up
</Button> </Button>
</NavbarItem> </NavbarItem>
</> </>
); );
}; };
export default LoginAndSignup; export default LoginAndSignup;

View File

@ -1,111 +1,111 @@
"use client"; "use client";
import { import {
Link, Link,
Navbar, Navbar,
NavbarBrand, NavbarBrand,
NavbarContent, NavbarContent,
NavbarItem, NavbarItem,
NavbarMenu, NavbarMenu,
NavbarMenuItem, NavbarMenuItem,
NavbarMenuToggle, NavbarMenuToggle,
} from "@heroui/react"; } from "@heroui/react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import React, { useState } from "react"; import React, { useState } from "react";
import { routes } from "../../../shared/config/routes"; import { routes } from "../../../shared/config/routes";
import LoginAndSignup from "./LoginAndSignup"; import LoginAndSignup from "./LoginAndSignup";
export const AcmeLogo = () => { export const AcmeLogo = () => {
return ( return (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36"> <svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path <path
clipRule="evenodd" clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z" d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor" fill="currentColor"
fillRule="evenodd" fillRule="evenodd"
/> />
</svg> </svg>
); );
}; };
const NavbarUI = () => { const NavbarUI = () => {
const pathNameNow = usePathname(); const pathNameNow = usePathname();
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const navbarItems = [ const navbarItems = [
{ {
title: "Home", title: "Home",
route: routes.home, route: routes.home,
}, },
{ {
title: "Explore", title: "Explore",
route: routes.explore, route: routes.explore,
}, },
{ {
title: "Trending", title: "Trending",
route: routes.trending, route: routes.trending,
}, },
{ {
title: "Genres", title: "Genres",
route: routes.genres, route: routes.genres,
}, },
{ {
title: "Schedule", title: "Schedule",
route: routes.schedule, route: routes.schedule,
}, },
]; ];
return ( return (
<Navbar onMenuOpenChange={setIsMenuOpen} maxWidth="full"> <Navbar onMenuOpenChange={setIsMenuOpen} maxWidth="full">
<NavbarContent> <NavbarContent>
<NavbarMenuToggle <NavbarMenuToggle
aria-label={isMenuOpen ? "Close menu" : "Open menu"} aria-label={isMenuOpen ? "Close menu" : "Open menu"}
className="sm:hidden" className="sm:hidden"
/> />
<NavbarBrand> <NavbarBrand>
<AcmeLogo /> <AcmeLogo />
<p className="font-bold text-inherit">ACME</p> <p className="font-bold text-inherit">ACME</p>
</NavbarBrand> </NavbarBrand>
</NavbarContent> </NavbarContent>
<NavbarContent className="hidden sm:flex gap-6" justify="center"> <NavbarContent className="hidden sm:flex gap-6" justify="center">
{navbarItems.map((item, index) => { {navbarItems.map((item, index) => {
const isActive = item.route === pathNameNow; const isActive = item.route === pathNameNow;
return ( return (
<NavbarItem key={index} isActive={isActive}> <NavbarItem key={index} isActive={isActive}>
<Link <Link
color={isActive ? "primary" : "foreground"} color={isActive ? "primary" : "foreground"}
href={isActive ? "" : item.route} href={isActive ? "" : item.route}
> >
{item.title} {item.title}
</Link> </Link>
</NavbarItem> </NavbarItem>
); );
})} })}
</NavbarContent> </NavbarContent>
<NavbarContent justify="end" className="gap-1"> <NavbarContent justify="end" className="gap-1">
<LoginAndSignup /> <LoginAndSignup />
</NavbarContent> </NavbarContent>
<NavbarMenu> <NavbarMenu>
{navbarItems.map((item, index) => ( {navbarItems.map((item, index) => (
<NavbarMenuItem key={`${item}-${index}`}> <NavbarMenuItem key={`${item}-${index}`}>
<Link <Link
className="w-full" className="w-full"
color="foreground" color="foreground"
href={item.route} href={item.route}
size="lg" size="lg"
> >
{item.title} {item.title}
</Link> </Link>
</NavbarMenuItem> </NavbarMenuItem>
))} ))}
</NavbarMenu> </NavbarMenu>
</Navbar> </Navbar>
); );
}; };
export default NavbarUI; export default NavbarUI;