♻️ 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:
14
.env.example
14
.env.example
@ -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=
|
||||
@ -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
|
||||
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@ -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
7
.gitignore
vendored
@ -31,7 +31,7 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@ -39,8 +39,3 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
scripts/git-multipush.ts
|
||||
/app/debug/
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
@ -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
|
||||
@ -1,8 +0,0 @@
|
||||
import OauthCallbackHandler from "@/features/oauth-callback/pages/callbackHandler";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <OauthCallbackHandler />;
|
||||
};
|
||||
|
||||
export default page;
|
||||
@ -1,5 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Login | Nounoz TV",
|
||||
};
|
||||
@ -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;
|
||||
@ -1,5 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sign Up | Nounoz TV",
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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"],
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>Explore Page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>Genre Page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
@ -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;
|
||||
@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>Schedule Page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>Trending Page</div>;
|
||||
};
|
||||
|
||||
export default page;
|
||||
@ -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..." });
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
131
app/globals.css
131
app/globals.css
@ -1,5 +1,126 @@
|
||||
@import "tailwindcss";
|
||||
@plugin './hero.ts';
|
||||
/* Note: You may need to change the path to fit your project structure */
|
||||
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@ -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
7
app/home/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const page = () => {
|
||||
redirect("/");
|
||||
};
|
||||
|
||||
export default page;
|
||||
@ -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 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 (
|
||||
<html lang="en" className="dark">
|
||||
<body>
|
||||
<NextTopLoader easing="ease" showSpinner={false} />
|
||||
<GeistFontProvider>
|
||||
<HeroUIWrapper>{children}</HeroUIWrapper>
|
||||
</GeistFontProvider>
|
||||
<html lang="en" className={`${inter.variable} dark`}>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<div className="max-w-400 mx-auto relative">
|
||||
<Navbar />
|
||||
<div className="pt-16">{children}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
}
|
||||
|
||||
5
app/page.tsx
Normal file
5
app/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import HomeIndex from "@/features/home";
|
||||
|
||||
export default function Page() {
|
||||
return <HomeIndex />;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
24
components.json
Normal file
24
components.json
Normal 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": {}
|
||||
}
|
||||
@ -1,16 +1,18 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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>;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,6 +0,0 @@
|
||||
export interface CallbackFromBackend<T> {
|
||||
success: boolean;
|
||||
status: number;
|
||||
message: string;
|
||||
data?: T;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export interface ResponseRequestOauthUrl {
|
||||
data: string;
|
||||
message: string;
|
||||
status: string;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const Step2ProvisionRegister = () => {
|
||||
return <div>Step2ProvisionRegister</div>;
|
||||
};
|
||||
|
||||
export default Step2ProvisionRegister;
|
||||
7
features/home/index.tsx
Normal file
7
features/home/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
const HomeIndex = () => {
|
||||
return <div className="text-center w-full">HomePage</div>;
|
||||
};
|
||||
|
||||
export default HomeIndex;
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
@ -1,9 +0,0 @@
|
||||
export interface ParamProps {
|
||||
params: { provider: string[] };
|
||||
searchParams:
|
||||
| string
|
||||
| string[][]
|
||||
| Record<string, string>
|
||||
| URLSearchParams
|
||||
| undefined;
|
||||
}
|
||||
@ -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
1
global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module "*.css";
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,42 +1,7 @@
|
||||
import {withSentryConfig} from "@sentry/nextjs";
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
env: {
|
||||
NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN,
|
||||
}
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
// For all available options, see:
|
||||
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
|
||||
|
||||
org: process.env.NEXT_PUBLIC_SENTRY_ORG || "",
|
||||
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT || "",
|
||||
sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL || "",
|
||||
|
||||
// Only print logs for uploading source maps in CI
|
||||
silent: !process.env.CI,
|
||||
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: "/monitoring",
|
||||
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: true
|
||||
});
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
56
package.json
56
package.json
@ -2,50 +2,42 @@
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"commit": "bunx git-cz",
|
||||
"push": "bun ./scripts/git-multipush.ts",
|
||||
"env:publish": "bun ./scripts/create-env-example.ts"
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.2",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@iconify/react": "^6.0.0",
|
||||
"@sentry/nextjs": "10",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"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",
|
||||
"@base-ui/react": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "16.1.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^3.8.16",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.64.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"zod": "^4.1.12"
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"shadcn": "^3.6.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.5",
|
||||
"postcss": "^8.5.6",
|
||||
"eslint-config-next": "16.1.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "cz-emoji"
|
||||
}
|
||||
}
|
||||
"ignoreScripts": [
|
||||
"sharp",
|
||||
"unrs-resolver"
|
||||
],
|
||||
"trustedDependencies": [
|
||||
"sharp",
|
||||
"unrs-resolver"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -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;
|
||||
@ -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
BIN
public/logo/astofo-long.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
@ -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);
|
||||
}
|
||||
@ -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.");
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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;
|
||||
@ -11,7 +11,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"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"]
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
Reference in New Issue
Block a user