🔒 (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
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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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",
};

View File

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

View File

@ -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",
};

View File

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

View File

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

View File

@ -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"],
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 *));

View File

@ -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();

View File

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

View File

@ -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<ResponseRequestOauthUrl>;
// 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<ResponseRequestOauthUrl>();
} 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<ResponseRequestOauthUrl>;
// 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<ResponseRequestOauthUrl>();
} catch (error) {
throw new Error(JSON.stringify(error));
}
// Redirect to the OAuth provider's authorization page
redirect((await oauthData).data);
};
export default requestOauthUrl;

View File

@ -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<ServerRequestCallback> => {
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<ServerRequestCallback> => {
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);
}
};

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";
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: <SecurityCheckup />,
securityCheckupFailed: <SecurityCheckupFailed />,
SecurityCheckupSuccessed: <Login />,
};
// 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 */}
<main>{componentFlow}</main>
</>
);
};
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: <SecurityCheckup />,
securityCheckupFailed: <SecurityCheckupFailed />,
SecurityCheckupSuccessed: <Login />,
};
// 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 */}
<main>{componentFlow}</main>
</>
);
};
export default LoginPage;

View File

@ -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<JSX.Element>(
<SecurityCheckup />
);
/**
* 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: <SecurityCheckupFailed />,
SecurityCheckupSuccessed: <Signup changeCurrentPage={setComponentFlow} />,
};
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 */}
<main>{componentFlow}</main>
</>
);
};
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<JSX.Element>(
<SecurityCheckup />
);
/**
* 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: <SecurityCheckupFailed />,
SecurityCheckupSuccessed: <Signup changeCurrentPage={setComponentFlow} />,
};
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 */}
<main>{componentFlow}</main>
</>
);
};
export default SignupPage;

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<div className="w-full flex flex-col gap-2 mt-4">
{/* Render OAuth provider buttons */}
{oauthProvidersList.length > 0 ? (
oauthProvidersList.map((provider, index) => {
return (
<Button
key={index}
className="w-full hover:bg-neutral-800"
variant="bordered"
startContent={<Icon className="w-4 h-4" icon={provider.icon} />}
onPress={() =>
startOauthProcess({
name: provider.name,
endpoint: provider.req_endpoint,
})
}
isLoading={loadingButton}
>
Continue with {provider.name}
</Button>
);
})
) : (
<Button className="w-full" variant="ghost" isDisabled>
No login options available via third-party providers
</Button>
)}
</div>
);
};
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 (
<div className="w-full flex flex-col gap-2 mt-4">
{/* Render OAuth provider buttons */}
{oauthProvidersList.length > 0 ? (
oauthProvidersList.map((provider, index) => {
return (
<Button
key={index}
className="w-full hover:bg-neutral-800"
variant="bordered"
startContent={<Icon className="w-4 h-4" icon={provider.icon} />}
onPress={() =>
startOauthProcess({
name: provider.name,
endpoint: provider.req_endpoint,
})
}
isLoading={loadingButton}
>
Continue with {provider.name}
</Button>
);
})
) : (
<Button className="w-full" variant="ghost" isDisabled>
No login options available via third-party providers
</Button>
)}
</div>
);
};
export default OAuthProviders;

View File

@ -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<RegisterInputs>();
setValue("fullname", fullname);
const [submitStatus, setSubmitStatus] = useState(false);
const onSubmit: SubmitHandler<RegisterInputs> = 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 (
<div className="mt-6 px-3">
<Form className="flex flex-col gap-1.5" onSubmit={handleSubmit(onSubmit)}>
<Input
{...register("email")}
className="w-full "
label="Email"
type="email"
variant="bordered"
classNames={{
input: "text-md font-light pt-4",
inputWrapper: "flex gap-10",
}}
/>
<Input
{...register("password")}
className="w-full "
label="Password"
type="password"
variant="bordered"
classNames={{
input: "text-md font-light pt-4",
inputWrapper: "flex gap-10",
}}
/>
<Input
{...register("confirmPassword")}
className="w-full "
label="Confirm Password"
type="password"
variant="bordered"
classNames={{
input: "text-md font-light pt-4",
inputWrapper: "flex gap-10",
}}
/>
<Button
type="submit"
className="mt-1.5 w-full"
color="primary"
isLoading={submitStatus}
>
Continue
</Button>
</Form>
</div>
);
};
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<RegisterInputs>({
resolver: zodResolver(registerFormSchema),
});
setValue("fullname", fullname);
const [submitStatus, setSubmitStatus] = useState(false);
const onSubmit: SubmitHandler<RegisterInputs> = 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 (
<div className="mt-6 px-3">
<Form className="flex flex-col gap-1.5" onSubmit={handleSubmit(onSubmit)}>
<Input
{...register("email")}
className="w-full "
label="Email"
type="email"
variant="bordered"
isInvalid={errors.email ? true : false}
errorMessage={errors.email?.message}
classNames={{
input: "text-md font-light pt-4",
inputWrapper: "flex gap-10",
}}
/>
<Input
{...register("password")}
className="w-full "
label="Password"
type="password"
variant="bordered"
isInvalid={errors.password ? true : false}
errorMessage={errors.password?.message}
classNames={{
input: "text-md font-light pt-4",
inputWrapper: "flex gap-10",
}}
/>
<Input
{...register("confirmPassword")}
className="w-full "
label="Confirm Password"
type="password"
variant="bordered"
isInvalid={errors.confirmPassword ? true : false}
errorMessage={errors.confirmPassword?.message}
classNames={{
input: "text-md font-light pt-4",
inputWrapper: "flex gap-10",
}}
/>
<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";
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<Object>} 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<ServerRequestCallback> => {
// 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<Object>} 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<ServerRequestCallback> => {
// 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);
}
};

View File

@ -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: <SecurityCheckup />,
securityCheckupFailed: <SecurityCheckupFailed />,
proceedCallback: <LoadingProcess />,
};
// 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 */}
<main>{componentFlow}</main>
</>
);
};
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: <SecurityCheckup />,
securityCheckupFailed: <SecurityCheckupFailed />,
proceedCallback: <LoadingProcess />,
};
// 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 */}
<main>{componentFlow}</main>
</>
);
};
export default OauthCallbackHandler;

View File

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

View File

@ -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: (
<Button
size="sm"
variant="flat"
onPress={() => (window.location.href = routes.login)}
>
Try again
</Button>
),
});
}
} catch (error) {
console.log(error);
addToast({
title: "😵‍💫 Oops, lost connection!",
description: "Check your internet and try again",
color: "danger",
timeout: 0,
endContent: (
<Button
size="sm"
variant="flat"
onPress={() => (window.location.href = routes.login)}
>
Reload
</Button>
),
});
}
});
return (
<div className="w-full flex flex-col items-center text-center mt-[26vh]">
<CircularProgress aria-label="Loading..." size="lg" />
<div className="mt-4">
<h1 className="text-lg text-neutral-200">Please wait...</h1>
<p className="text-sm text-neutral-400">
Your request is being processed
</p>
</div>
</div>
);
};
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: (
<Button
size="sm"
variant="flat"
onPress={() => (window.location.href = routes.login)}
>
Try again
</Button>
),
});
}
} catch (error) {
console.log(error);
addToast({
title: "😵‍💫 Oops, lost connection!",
description: "Check your internet and try again",
color: "danger",
timeout: 0,
endContent: (
<Button
size="sm"
variant="flat"
onPress={() => (window.location.href = routes.login)}
>
Reload
</Button>
),
});
}
});
return (
<div className="w-full flex flex-col items-center text-center mt-[26vh]">
<CircularProgress aria-label="Loading..." size="lg" />
<div className="mt-4">
<h1 className="text-lg text-neutral-200">Please wait...</h1>
<p className="text-sm text-neutral-400">
Your request is being processed
</p>
</div>
</div>
);
};
export default LoadingProcess;

View File

@ -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
});

View File

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

View File

@ -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 (
<HeroUIProvider navigate={router.push}>
<ToastProvider />
{mounted ? (
<NextThemesProvider attribute={"class"} defaultTheme="dark">
<main>{children}</main>
</NextThemesProvider>
) : (
<main className="dark text-foreground bg-background">{children}</main>
)}
</HeroUIProvider>
);
};
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 (
<HeroUIProvider navigate={router.push}>
<ToastProvider />
{mounted ? (
<NextThemesProvider attribute={"class"} defaultTheme="dark">
<main>{children}</main>
</NextThemesProvider>
) : (
<main className="dark text-foreground bg-background">{children}</main>
)}
</HeroUIProvider>
);
};
export default HeroUIWrapper;

View File

@ -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 <div className={`${Geist.className}`}>{children}</div>;
};
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 <div className={`${Geist.className}`}>{children}</div>;
};
export default GeistFontProvider;

View File

@ -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);
}

View File

@ -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.");

View File

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

View File

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

View File

@ -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"],
},
};
};

View File

@ -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",
};

View File

@ -1,21 +1,21 @@
export const API_BASE_URL =
process.env.MAIN_BACKEND_API_URL ?? "http://localhost";
const apiFetch = async <T = unknown>(
path: string,
init?: RequestInit
): Promise<T> => {
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 <T = unknown>(
path: string,
init?: RequestInit
): Promise<T> => {
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;

View File

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

View File

@ -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<string>();
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<string>();
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]);
}

View File

@ -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,
});

View File

@ -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;
}
};

View File

@ -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;
};

View File

@ -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 (
<>
<NavbarItem className="hidden lg:flex">
<Button
color="primary"
variant="light"
className="font-medium"
onPress={() => openPopupWindow(routes.login)}
>
Login
</Button>
</NavbarItem>
<NavbarItem>
<Button
color="primary"
variant="solid"
radius="sm"
onPress={() => openPopupWindow(routes.signup)}
>
Sign Up
</Button>
</NavbarItem>
</>
);
};
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 (
<>
<NavbarItem className="hidden lg:flex">
<Button
color="primary"
variant="light"
className="font-medium"
onPress={() => openPopupWindow(routes.login)}
>
Login
</Button>
</NavbarItem>
<NavbarItem>
<Button
color="primary"
variant="solid"
radius="sm"
onPress={() => openPopupWindow(routes.signup)}
>
Sign Up
</Button>
</NavbarItem>
</>
);
};
export default LoginAndSignup;

View File

@ -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 (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
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"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
};
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 (
<Navbar onMenuOpenChange={setIsMenuOpen} maxWidth="full">
<NavbarContent>
<NavbarMenuToggle
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
className="sm:hidden"
/>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
</NavbarContent>
<NavbarContent className="hidden sm:flex gap-6" justify="center">
{navbarItems.map((item, index) => {
const isActive = item.route === pathNameNow;
return (
<NavbarItem key={index} isActive={isActive}>
<Link
color={isActive ? "primary" : "foreground"}
href={isActive ? "" : item.route}
>
{item.title}
</Link>
</NavbarItem>
);
})}
</NavbarContent>
<NavbarContent justify="end" className="gap-1">
<LoginAndSignup />
</NavbarContent>
<NavbarMenu>
{navbarItems.map((item, index) => (
<NavbarMenuItem key={`${item}-${index}`}>
<Link
className="w-full"
color="foreground"
href={item.route}
size="lg"
>
{item.title}
</Link>
</NavbarMenuItem>
))}
</NavbarMenu>
</Navbar>
);
};
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 (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
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"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
};
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 (
<Navbar onMenuOpenChange={setIsMenuOpen} maxWidth="full">
<NavbarContent>
<NavbarMenuToggle
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
className="sm:hidden"
/>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
</NavbarContent>
<NavbarContent className="hidden sm:flex gap-6" justify="center">
{navbarItems.map((item, index) => {
const isActive = item.route === pathNameNow;
return (
<NavbarItem key={index} isActive={isActive}>
<Link
color={isActive ? "primary" : "foreground"}
href={isActive ? "" : item.route}
>
{item.title}
</Link>
</NavbarItem>
);
})}
</NavbarContent>
<NavbarContent justify="end" className="gap-1">
<LoginAndSignup />
</NavbarContent>
<NavbarMenu>
{navbarItems.map((item, index) => (
<NavbarMenuItem key={`${item}-${index}`}>
<Link
className="w-full"
color="foreground"
href={item.route}
size="lg"
>
{item.title}
</Link>
</NavbarMenuItem>
))}
</NavbarMenu>
</Navbar>
);
};
export default NavbarUI;