♻️ 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,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;