♻️ refactor: all codebase

Completely refactoring the entire old codebase with a new codebase. This change also altered most of the core UI from the old codebase, replacing it with Shadcn with some customizations.
This commit is contained in:
2026-01-07 08:44:48 +07:00
parent fbcb575a36
commit a82e7a7424
95 changed files with 1143 additions and 3303 deletions

View File

@ -1,14 +0,0 @@
APP_NAME="Syzne TV"
APP_ENV="development"
APP_DOMAIN=
APP_PORT=3000
MAIN_BACKEND_API_URL=
MAIN_BACKEND_API_KEY=
API_KEY=
JWT_TOKEN=
NEXT_PUBLIC_SENTRY_ORG=
NEXT_PUBLIC_SENTRY_PROJECT=
NEXT_PUBLIC_SENTRY_URL=
NEXT_PUBLIC_SENTRY_DSN=

View File

@ -1,28 +0,0 @@
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 +0,0 @@
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

7
.gitignore vendored
View File

@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env .env*
# vercel # vercel
.vercel .vercel
@ -39,8 +39,3 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
scripts/git-multipush.ts
/app/debug/
# Sentry Config File
.env.sentry-build-plugin

View File

@ -1,18 +0,0 @@
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 +0,0 @@
import OauthCallbackHandler from "@/features/oauth-callback/pages/callbackHandler";
import React from "react";
const page = () => {
return <OauthCallbackHandler />;
};
export default page;

View File

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

View File

@ -1,11 +0,0 @@
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 +0,0 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Sign Up | Nounoz TV",
};

View File

@ -1,11 +0,0 @@
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 +0,0 @@
"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 +0,0 @@
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,18 +0,0 @@
import Button from "./button";
import { metadata } from "./metadata";
export { metadata };
import React from "react";
const page = () => {
return (
<main>
<div className="text-center text-3xl mt-6">Nounoz TV</div>
<div>
<Button />
</div>
</main>
);
};
export default page;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +0,0 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
class SentryExampleAPIError extends Error {
constructor(message: string | undefined) {
super(message);
this.name = "SentryExampleAPIError";
}
}
// A faulty API route to test Sentry's error monitoring
export function GET() {
throw new SentryExampleAPIError("This error is raised on the backend called by the example page.");
return NextResponse.json({ data: "Testing Sentry Error..." });
}

View File

@ -1,23 +0,0 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
{/* `NextError` is the default Next.js error page component. Its type
definition requires a `statusCode` prop. However, since the App Router
does not expose status codes for errors, we simply pass 0 to render a
generic error message. */}
<NextError statusCode={0} />
</body>
</html>
);
}

View File

@ -1,5 +1,126 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin './hero.ts'; @import "tw-animate-css";
/* Note: You may need to change the path to fit your project structure */ @import "shadcn/tailwind.css";
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.67 0.16 58);
--primary-foreground: oklch(0.99 0.02 95);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.58 0.22 27);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.88 0.15 92);
--chart-2: oklch(0.77 0.16 70);
--chart-3: oklch(0.67 0.16 58);
--chart-4: oklch(0.56 0.15 49);
--chart-5: oklch(0.47 0.12 46);
--radius: 0.45rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.67 0.16 58);
--sidebar-primary-foreground: oklch(0.99 0.02 95);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.77 0.16 70);
--primary-foreground: oklch(0.28 0.07 46);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.88 0.15 92);
--chart-2: oklch(0.77 0.16 70);
--chart-3: oklch(0.67 0.16 58);
--chart-4: oklch(0.56 0.15 49);
--chart-5: oklch(0.47 0.12 46);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.77 0.16 70);
--sidebar-primary-foreground: oklch(0.28 0.07 46);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,5 +0,0 @@
// 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();

7
app/home/page.tsx Normal file
View File

@ -0,0 +1,7 @@
import { redirect } from "next/navigation";
const page = () => {
redirect("/");
};
export default page;

View File

@ -1,20 +1,40 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono, Inter } from "next/font/google";
import Navbar from "@/shared/widgets/navbar/components/Navbar";
import "./globals.css"; import "./globals.css";
import GeistFontProvider from "@/providers/fonts/GeistFontProvider";
import HeroUIWrapper from "@/providers/HeroUIWrapper";
import NextTopLoader from "nextjs-toploader";
import React from "react";
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return ( return (
<html lang="en" className="dark"> <html lang="en" className={`${inter.variable} dark`}>
<body> <body
<NextTopLoader easing="ease" showSpinner={false} /> className={`${geistSans.variable} ${geistMono.variable} antialiased`}
<GeistFontProvider> >
<HeroUIWrapper>{children}</HeroUIWrapper> <div className="max-w-400 mx-auto relative">
</GeistFontProvider> <Navbar />
<div className="pt-16">{children}</div>
</div>
</body> </body>
</html> </html>
); );
}; }
export default RootLayout;

5
app/page.tsx Normal file
View File

@ -0,0 +1,5 @@
import HomeIndex from "@/features/home";
export default function Page() {
return <HomeIndex />;
}

View File

@ -1,209 +0,0 @@
"use client";
import Head from "next/head";
import * as Sentry from "@sentry/nextjs";
import { useState, useEffect } from "react";
class SentryExampleFrontendError extends Error {
constructor(message: string | undefined) {
super(message);
this.name = "SentryExampleFrontendError";
}
}
export default function Page() {
const [hasSentError, setHasSentError] = useState(false);
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
async function checkConnectivity() {
const result = await Sentry.diagnoseSdkConnectivity();
setIsConnected(result !== 'sentry-unreachable');
}
checkConnectivity();
}, []);
return (
<div>
<Head>
<title>sentry-example-page</title>
<meta name="description" content="Test Sentry for your Next.js app!" />
</Head>
<main>
<div className="flex-spacer" />
<svg height="40" width="40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.85 2.995a3.698 3.698 0 0 1 1.353 1.354l16.303 28.278a3.703 3.703 0 0 1-1.354 5.053 3.694 3.694 0 0 1-1.848.496h-3.828a31.149 31.149 0 0 0 0-3.09h3.815a.61.61 0 0 0 .537-.917L20.523 5.893a.61.61 0 0 0-1.057 0l-3.739 6.494a28.948 28.948 0 0 1 9.63 10.453 28.988 28.988 0 0 1 3.499 13.78v1.542h-9.852v-1.544a19.106 19.106 0 0 0-2.182-8.85 19.08 19.08 0 0 0-6.032-6.829l-1.85 3.208a15.377 15.377 0 0 1 6.382 12.484v1.542H3.696A3.694 3.694 0 0 1 0 34.473c0-.648.17-1.286.494-1.849l2.33-4.074a8.562 8.562 0 0 1 2.689 1.536L3.158 34.17a.611.611 0 0 0 .538.917h8.448a12.481 12.481 0 0 0-6.037-9.09l-1.344-.772 4.908-8.545 1.344.77a22.16 22.16 0 0 1 7.705 7.444 22.193 22.193 0 0 1 3.316 10.193h3.699a25.892 25.892 0 0 0-3.811-12.033 25.856 25.856 0 0 0-9.046-8.796l-1.344-.772 5.269-9.136a3.698 3.698 0 0 1 3.2-1.849c.648 0 1.285.17 1.847.495Z" fill="currentcolor"/>
</svg>
<h1>
sentry-example-page
</h1>
<p className="description">
Click the button below, and view the sample error on the Sentry <a target="_blank" href="http://localhost:3100/organizations/syzne-tv/issues/?project=1">Issues Page</a>.
For more details about setting up Sentry, <a target="_blank"
href="https://docs.sentry.io/platforms/javascript/guides/nextjs/">read our docs</a>.
</p>
<button
type="button"
onClick={async () => {
await Sentry.startSpan({
name: 'Example Frontend/Backend Span',
op: 'test'
}, async () => {
const res = await fetch("/api/sentry-example-api");
if (!res.ok) {
setHasSentError(true);
}
});
throw new SentryExampleFrontendError("This error is raised on the frontend of the example page.");
}}
disabled={!isConnected}
>
<span>
Throw Sample Error
</span>
</button>
{hasSentError ? (
<p className="success">
Error sent to Sentry.
</p>
) : !isConnected ? (
<div className="connectivity-error">
<p>It looks like network requests to Sentry are being blocked, which will prevent errors from being captured. Try disabling your ad-blocker to complete the test.</p>
</div>
) : (
<div className="success_placeholder" />
)}
<div className="flex-spacer" />
</main>
<style>{`
main {
display: flex;
min-height: 100vh;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
padding: 16px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
}
h1 {
padding: 0px 4px;
border-radius: 4px;
background-color: rgba(24, 20, 35, 0.03);
font-family: monospace;
font-size: 20px;
line-height: 1.2;
}
p {
margin: 0;
font-size: 20px;
}
a {
color: #6341F0;
text-decoration: underline;
cursor: pointer;
@media (prefers-color-scheme: dark) {
color: #B3A1FF;
}
}
button {
border-radius: 8px;
color: white;
cursor: pointer;
background-color: #553DB8;
border: none;
padding: 0;
margin-top: 4px;
& > span {
display: inline-block;
padding: 12px 16px;
border-radius: inherit;
font-size: 20px;
font-weight: bold;
line-height: 1;
background-color: #7553FF;
border: 1px solid #553DB8;
transform: translateY(-4px);
}
&:hover > span {
transform: translateY(-8px);
}
&:active > span {
transform: translateY(0);
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
& > span {
transform: translateY(0);
border: none
}
}
}
.description {
text-align: center;
color: #6E6C75;
max-width: 500px;
line-height: 1.5;
font-size: 20px;
@media (prefers-color-scheme: dark) {
color: #A49FB5;
}
}
.flex-spacer {
flex: 1;
}
.success {
padding: 12px 16px;
border-radius: 8px;
font-size: 20px;
line-height: 1;
background-color: #00F261;
border: 1px solid #00BF4D;
color: #181423;
}
.success_placeholder {
height: 46px;
}
.connectivity-error {
padding: 12px 16px;
background-color: #E50045;
border-radius: 8px;
width: 500px;
color: #FFFFFF;
border: 1px solid #A80033;
text-align: center;
margin: 0;
}
.connectivity-error a {
color: #FFFFFF;
text-decoration: underline;
}
`}</style>
</div>
);
}

1748
bun.lock

File diff suppressed because it is too large Load Diff

24
components.json Normal file
View File

@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/shared/libs/shadcn/components",
"utils": "@/shared/lib/shadcn/utils",
"ui": "@/shared/libs/shadcn/ui",
"lib": "@/shared/libs/shadcn/lib",
"hooks": "@/shared/libs/shadcn/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

View File

@ -1,16 +1,18 @@
import { dirname } from "path"; import { defineConfig, globalIgnores } from "eslint/config";
import { fileURLToPath } from "url"; import nextVitals from "eslint-config-next/core-web-vitals";
import { FlatCompat } from "@eslint/eslintrc"; import nextTs from "eslint-config-next/typescript";
const __filename = fileURLToPath(import.meta.url); const eslintConfig = defineConfig([
const __dirname = dirname(__filename); ...nextVitals,
...nextTs,
const compat = new FlatCompat({ // Override default ignores of eslint-config-next.
baseDirectory: __dirname, globalIgnores([
}); // Default ignores of eslint-config-next:
".next/**",
const eslintConfig = [ "out/**",
...compat.extends("next/core-web-vitals", "next/typescript"), "build/**",
]; "next-env.d.ts",
]),
]);
export default eslintConfig; export default eslintConfig;

View File

@ -1,13 +0,0 @@
"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 +0,0 @@
"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,51 +0,0 @@
"use server";
import { apiErrorHandler } from "@/shared/lib/ky/errorHandler";
import { RegisterInputs } from "../ui/components/ProvisionInput";
import { ServerRequestCallback } from "@/shared/types/ServerRequestCallback";
import { generateRandomString } from "@/shared/helper/generateRandomString";
import { CallbackFromBackend } from "../types/callbackFromBackend";
import { api } from "@/shared/lib/ky/connector";
import { COOKIE_KEYS } from "@/shared/constants/cookie.key";
import { setCookie } from "@/shared/helper/cookies/set";
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 {
const payload = {
username:
data.fullname
.trim()
.split(" ")[0]
.toLowerCase()
.substring(0, 5) +
"_" +
generateRandomString(10),
name: data.fullname,
email: data.email,
password: data.password,
};
const callback = (await api
.post("users", { json: payload })
.json()) as CallbackFromBackend<string>;
await setCookie(COOKIE_KEYS.AUTH, callback.data!);
return {
success: true,
status: 200,
text: { message: "Registration successful" },
data: callback.data,
};
} catch (error) {
return apiErrorHandler(error);
}
};

View File

@ -1,21 +0,0 @@
import { z } from "zod";
export const registerFormSchema = z
.object({
fullname: z.string().min(4, "Full name must be at least 4 characters long"),
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 +0,0 @@
"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 +0,0 @@
"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,6 +0,0 @@
export interface CallbackFromBackend<T> {
success: boolean;
status: number;
message: string;
data?: T;
}

View File

@ -1,8 +0,0 @@
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 +0,0 @@
export interface ResponseRequestOauthUrl {
data: string;
message: string;
status: string;
}

View File

@ -1,42 +0,0 @@
"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 +0,0 @@
"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 +0,0 @@
"use client";
import React from "react";
import { Divider, Link } from "@heroui/react";
import { routes } from "@/shared/config/routes";
import OAuthProviders from "../components/OAuthProviders";
import ProvisionRegister from "../components/provision/main";
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">
<ProvisionRegister />
</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 +0,0 @@
"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,85 +0,0 @@
"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,118 +0,0 @@
"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,47 +0,0 @@
"use client";
import React, { JSX, useState } from "react";
import Step1ProvisionRegister from "./step1";
import { Form } from "@heroui/react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { registerFormSchema } from "@/features/auth/models/registerForm.schema";
import Step2ProvisionRegister from "./step2";
export type RegisterInputs = {
fullname: string;
email: string;
password: string;
confirmPassword: string;
};
const ProvisionRegister = () => {
const formMethods = useForm<RegisterInputs>({
resolver: zodResolver(registerFormSchema),
});
const { handleSubmit } = formMethods;
const sendProvisionData: SubmitHandler<RegisterInputs> = (data) => {};
const NextToStep2 = () => {
setCurrentComponent(<Step2ProvisionRegister />);
};
const [currentComponent, setCurrentComponent] = useState<JSX.Element>(
<Step1ProvisionRegister NextStep={NextToStep2} />
);
return (
<FormProvider {...formMethods}>
<Form
className="flex flex-col gap-1.5 "
onSubmit={handleSubmit(sendProvisionData)}
>
{currentComponent}
</Form>
</FormProvider>
);
};
export default ProvisionRegister;

View File

@ -1,39 +0,0 @@
"use client";
import { Button, Input } from "@heroui/react";
import React from "react";
import { useFormContext } from "react-hook-form";
const Step1ProvisionRegister = ({ NextStep }: { NextStep: () => void }) => {
const {
register,
formState: { errors },
} = useFormContext();
return (
<div className="w-full">
<Input
{...register("fullName")}
className="w-full"
label="Full Name"
type="email"
variant="bordered"
isInvalid={errors.fullname ? true : false}
errorMessage={errors.fullname?.message as string}
classNames={{
input: "text-md font-light pt-4",
inputWrapper: "flex gap-10",
}}
/>
<Button
type="button"
className="mt-1.5 w-full"
color="primary"
onPress={NextStep}
>
Continue
</Button>
</div>
);
};
export default Step1ProvisionRegister;

View File

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

7
features/home/index.tsx Normal file
View File

@ -0,0 +1,7 @@
"use client";
const HomeIndex = () => {
return <div className="text-center w-full">HomePage</div>;
};
export default HomeIndex;

View File

@ -1,83 +0,0 @@
"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 +0,0 @@
"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 +0,0 @@
export interface ParamProps {
params: { provider: string[] };
searchParams:
| string
| string[][]
| Record<string, string>
| URLSearchParams
| undefined;
}

View File

@ -1,73 +0,0 @@
"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;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "*.css";

View File

@ -1,30 +0,0 @@
// This file configures the initialization of Sentry on the client.
// The added config here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || "",
// Add optional integrations for additional features
integrations: [Sentry.replayIntegration()],
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Define how likely Replay events are sampled.
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// Define how likely Replay events are sampled when an error occurs.
replaysOnErrorSampleRate: 1.0,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;

View File

@ -1,13 +0,0 @@
import * as Sentry from '@sentry/nextjs';
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}
export const onRequestError = Sentry.captureRequestError;

View File

@ -1,42 +1,7 @@
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: {
NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN,
}
}; };
export default withSentryConfig(nextConfig, { export default 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

@ -2,50 +2,42 @@
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint"
"commit": "bunx git-cz",
"push": "bun ./scripts/git-multipush.ts",
"env:publish": "bun ./scripts/create-env-example.ts"
}, },
"dependencies": { "dependencies": {
"@heroui/react": "^2.8.2", "@base-ui/react": "^1.0.0",
"@hookform/resolvers": "^5.2.2", "class-variance-authority": "^0.7.1",
"@iconify/react": "^6.0.0", "clsx": "^2.1.1",
"@sentry/nextjs": "10", "lucide-react": "^0.562.0",
"@tailwindcss/postcss": "^4.1.11", "next": "16.1.1",
"commitizen": "^4.3.1",
"cz-emoji": "^1.3.2-canary.2",
"framer-motion": "^12.23.3",
"ky": "^1.8.2",
"nanoid": "^5.1.6",
"next": "15.3.5",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nextjs-toploader": "^3.8.16", "radix-ui": "^1.4.3",
"react": "^19.0.0", "react": "19.2.3",
"react-dom": "^19.0.0", "react-dom": "19.2.3",
"react-hook-form": "^7.64.0", "shadcn": "^3.6.3",
"tailwindcss": "^4.1.11", "tailwind-merge": "^3.4.0",
"zod": "^4.1.12" "tw-animate-css": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"autoprefixer": "^10.4.21",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.5", "eslint-config-next": "16.1.1",
"postcss": "^8.5.6", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5"
}, },
"config": { "ignoreScripts": [
"commitizen": { "sharp",
"path": "cz-emoji" "unrs-resolver"
} ],
} "trustedDependencies": [
"sharp",
"unrs-resolver"
]
} }

View File

@ -3,4 +3,5 @@ const config = {
"@tailwindcss/postcss": {}, "@tailwindcss/postcss": {},
}, },
}; };
export default config; export default config;

View File

@ -1,32 +0,0 @@
"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 +0,0 @@
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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

BIN
public/logo/astofo-long.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -1,49 +0,0 @@
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 +0,0 @@
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,19 +0,0 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || "",
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@ -1,18 +0,0 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || "",
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@ -1,17 +0,0 @@
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 +0,0 @@
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 +0,0 @@
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 +0,0 @@
export const routes = {
home: "/",
login: "/login",
signup: "/signup",
explore: "/explore",
trending: "/trending",
genres: "/genres",
schedule: "/schedule",
};

View File

@ -1,4 +0,0 @@
export const COOKIE_KEYS = {
AUTH: "auth_token",
CSRF: "csrf_token",
};

View File

@ -1,21 +0,0 @@
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,15 +0,0 @@
"use server";
import { cookies } from "next/headers";
export const setCookie = async (name: string, value: string, sec?: number) => {
(await cookies()).set({
name,
value,
httpOnly: true,
secure: process.env.NODE_ENV === "production",
path: "/",
sameSite: "lax",
maxAge: sec || Number(process.env.SESSION_EXPIRE),
});
};

View File

@ -1,11 +0,0 @@
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,7 +0,0 @@
import { customAlphabet } from "nanoid";
export const generateRandomString = (length: number = 10): string => {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return customAlphabet(characters, length)();
};

View File

@ -1,58 +0,0 @@
"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 +0,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 +0,0 @@
"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

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -0,0 +1,167 @@
import * as React from "react";
import { cva } from "class-variance-authority";
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui";
import { cn } from "@/shared/libs/shadcn/lib/utils";
import { ChevronDownIcon } from "lucide-react";
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"max-w-max group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"gap-0 group flex flex-1 list-none items-center justify-center",
className
)}
{...props}
/>
);
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
);
}
const navigationMenuTriggerStyle = cva(
"bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all focus-visible:ring-[3px] focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center disabled:pointer-events-none outline-none"
);
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
);
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:ring-foreground/10 p-1 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:duration-300 top-0 left-0 w-full group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none md:absolute md:w-auto",
className
)}
{...props}
/>
);
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:zoom-out-95 data-open:zoom-in-90 ring-foreground/10 rounded-lg shadow ring-1 duration-100 origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
);
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-active:focus:bg-muted data-active:hover:bg-muted data-active:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4 [[data-slot=navigation-menu-content]_&]:rounded-md",
className
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border rounded-tl-sm shadow-md relative top-[60%] h-2 w-2 rotate-45" />
</NavigationMenuPrimitive.Indicator>
);
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
};

View File

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

View File

@ -0,0 +1,20 @@
"use client";
import Image from "next/image";
import NavigationLink from "./NavigationLink";
const Navbar = () => {
return (
<div className="absolute z-10 top-0 w-full h-16 flex items-center">
<Image
src="/logo/astofo-long.png"
alt="Astofo Logo"
width={120}
height={0}
draggable={false}
/>
<NavigationLink />
</div>
);
};
export default Navbar;

View File

@ -0,0 +1,97 @@
"use client";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/shared/libs/shadcn/ui/navigation-menu";
import Link from "next/link";
const NavigationLink = () => {
return (
<div className="pl-10">
<NavigationMenu viewport={false}>
<NavigationMenuList className="flex-wrap">
<NavigationMenuItem>
<NavigationMenuLink asChild>
<Link href="/season" className="text-sm">
Season
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild>
<Link href="/genres" className="text-sm">
Genres
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild>
<Link href="/trending" className="text-sm">
Trending
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger className="font-normal">
Media
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-50 gap-4">
<li>
<NavigationMenuLink asChild>
<Link href="/release/finished" className="text-sm">
TV
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link href="/release/onair" className="text-sm">
Movie
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link href="/release/upcoming" className="text-sm">
OVA
</Link>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger className="font-normal">
Release
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-50 gap-0">
<li>
<NavigationMenuLink asChild>
<Link href="/release/finished" className="text-sm">
Finished
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link href="/release/onair" className="text-sm">
On Air
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link href="/release/upcoming" className="text-sm">
Upcoming
</Link>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
);
};
export default NavigationLink;

View File

@ -11,7 +11,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@ -22,6 +22,13 @@
"@/*": ["./*"] "@/*": ["./*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@ -1,37 +0,0 @@
"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 +0,0 @@
"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;