♻️ 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:
@ -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;
|
||||
@ -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;
|
||||
@ -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"],
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -1,9 +0,0 @@
|
||||
export const routes = {
|
||||
home: "/",
|
||||
login: "/login",
|
||||
signup: "/signup",
|
||||
explore: "/explore",
|
||||
trending: "/trending",
|
||||
genres: "/genres",
|
||||
schedule: "/schedule",
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
export const COOKIE_KEYS = {
|
||||
AUTH: "auth_token",
|
||||
CSRF: "csrf_token",
|
||||
};
|
||||
@ -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;
|
||||
@ -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),
|
||||
});
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
export const generateRandomString = (length: number = 10): string => {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return customAlphabet(characters, length)();
|
||||
};
|
||||
@ -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]);
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
6
shared/libs/shadcn/lib/utils.ts
Normal file
6
shared/libs/shadcn/lib/utils.ts
Normal 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))
|
||||
}
|
||||
167
shared/libs/shadcn/ui/navigation-menu.tsx
Normal file
167
shared/libs/shadcn/ui/navigation-menu.tsx
Normal 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,
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
export type ServerRequestCallback = {
|
||||
success: boolean;
|
||||
status: number;
|
||||
text: { message: string };
|
||||
data?: any;
|
||||
error?: unknown;
|
||||
};
|
||||
20
shared/widgets/navbar/components/Navbar.tsx
Normal file
20
shared/widgets/navbar/components/Navbar.tsx
Normal 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;
|
||||
97
shared/widgets/navbar/components/NavigationLink.tsx
Normal file
97
shared/widgets/navbar/components/NavigationLink.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user