remove older project

This commit is contained in:
vergiLgood1 2025-02-28 19:26:26 +07:00
parent 10ce404a1d
commit ca90871b22
263 changed files with 11101 additions and 7665 deletions

View File

@ -0,0 +1,4 @@
# Update these with your Supabase details from your project settings > API
# https://app.supabase.com/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

41
sigap-website-v2/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

104
sigap-website-v2/README.md Normal file
View File

@ -0,0 +1,104 @@
<a href="https://demo-nextjs-with-supabase.vercel.app/">
<img alt="Next.js and Supabase Starter Kit - the fastest way to build apps with Next.js and Supabase" src="https://demo-nextjs-with-supabase.vercel.app/opengraph-image.png">
<h1 align="center">Next.js and Supabase Starter Kit</h1>
</a>
<p align="center">
The fastest way to build apps with Next.js and Supabase
</p>
<p align="center">
<a href="#features"><strong>Features</strong></a> ·
<a href="#demo"><strong>Demo</strong></a> ·
<a href="#deploy-to-vercel"><strong>Deploy to Vercel</strong></a> ·
<a href="#clone-and-run-locally"><strong>Clone and run locally</strong></a> ·
<a href="#feedback-and-issues"><strong>Feedback and issues</strong></a>
<a href="#more-supabase-examples"><strong>More Examples</strong></a>
</p>
<br/>
## Features
- Works across the entire [Next.js](https://nextjs.org) stack
- App Router
- Pages Router
- Middleware
- Client
- Server
- It just works!
- supabase-ssr. A package to configure Supabase Auth to use cookies
- Styling with [Tailwind CSS](https://tailwindcss.com)
- Components with [shadcn/ui](https://ui.shadcn.com/)
- Optional deployment with [Supabase Vercel Integration and Vercel deploy](#deploy-your-own)
- Environment variables automatically assigned to Vercel project
## Demo
You can view a fully working demo at [demo-nextjs-with-supabase.vercel.app](https://demo-nextjs-with-supabase.vercel.app/).
## Deploy to Vercel
Vercel deployment will guide you through creating a Supabase account and project.
After installation of the Supabase integration, all relevant environment variables will be assigned to the project so the deployment is fully functioning.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&project-name=nextjs-with-supabase&repository-name=nextjs-with-supabase&demo-title=nextjs-with-supabase&demo-description=This+starter+configures+Supabase+Auth+to+use+cookies%2C+making+the+user%27s+session+available+throughout+the+entire+Next.js+app+-+Client+Components%2C+Server+Components%2C+Route+Handlers%2C+Server+Actions+and+Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2Fopengraph-image.png)
The above will also clone the Starter kit to your GitHub, you can clone that locally and develop locally.
If you wish to just develop locally and not deploy to Vercel, [follow the steps below](#clone-and-run-locally).
## Clone and run locally
1. You'll first need a Supabase project which can be made [via the Supabase dashboard](https://database.new)
2. Create a Next.js app using the Supabase Starter template npx command
```bash
npx create-next-app --example with-supabase with-supabase-app
```
```bash
yarn create next-app --example with-supabase with-supabase-app
```
```bash
pnpm create next-app --example with-supabase with-supabase-app
```
3. Use `cd` to change into the app's directory
```bash
cd with-supabase-app
```
4. Rename `.env.example` to `.env.local` and update the following:
```
NEXT_PUBLIC_SUPABASE_URL=[INSERT SUPABASE PROJECT URL]
NEXT_PUBLIC_SUPABASE_ANON_KEY=[INSERT SUPABASE PROJECT API ANON KEY]
```
Both `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` can be found in [your Supabase project's API settings](https://app.supabase.com/project/_/settings/api)
5. You can now run the Next.js local development server:
```bash
npm run dev
```
The starter kit should now be running on [localhost:3000](http://localhost:3000/).
6. This template comes with the default shadcn/ui style initialized. If you instead want other ui.shadcn styles, delete `components.json` and [re-install shadcn/ui](https://ui.shadcn.com/docs/installation/next)
> Check out [the docs for Local Development](https://supabase.com/docs/guides/getting-started/local-development) to also run Supabase locally.
## Feedback and issues
Please file feedback and issues over on the [Supabase GitHub org](https://github.com/supabase/supabase/issues/new/choose).
## More Supabase examples
- [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments)
- [Cookie-based Auth and the Next.js 13 App Router (free course)](https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF)
- [Supabase Auth and the Next.js App Router](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs)

View File

@ -0,0 +1,40 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const forgotPasswordAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
const callbackUrl = formData.get("callbackUrl")?.toString();
if (!email) {
return encodedRedirect("error", "/forgot-password", "Email is required");
}
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`,
});
if (error) {
console.error(error.message);
return encodedRedirect(
"error",
"/forgot-password",
"Could not reset password",
);
}
if (callbackUrl) {
return redirect(callbackUrl);
}
return encodedRedirect(
"success",
"/forgot-password",
"Check your email for a link to reset your password.",
);
};

View File

@ -0,0 +1,43 @@
"use server";
import { encodedRedirect } from "@/utils/utils";
import { createClient } from "@/utils/supabase/server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const resetPasswordAction = async (formData: FormData) => {
const supabase = await createClient();
const password = formData.get("password") as string;
const confirmPassword = formData.get("confirmPassword") as string;
if (!password || !confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password and confirm password are required"
);
}
if (password !== confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Passwords do not match"
);
}
const { error } = await supabase.auth.updateUser({
password: password,
});
if (error) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password update failed"
);
}
encodedRedirect("success", "/protected/reset-password", "Password updated");
};

View File

@ -0,0 +1,100 @@
// import { createClient } from "@/utils/supabase/server";
// export async function sendContactEmail(formData: {
// name: string;
// email: string;
// phone: string;
// typeMessage: string;
// message: string;
// }) {
// try {
// // Initialize Supabase
// const supabase = await createClient();
// const { resend } = useResend();
// // Get message type label
// const messageTypeLabel =
// typeMessageMap.get(formData.typeMessage) || "Unknown";
// // Save to Supabase
// const { data: contactData, error: contactError } = await supabase
// .from("contact_messages")
// .insert([
// {
// name: formData.name,
// email: formData.email,
// phone: formData.phone,
// message_type: formData.typeMessage,
// message_type_label: messageTypeLabel,
// message: formData.message,
// status: "new",
// },
// ])
// .select();
// if (contactError) {
// console.error("Error saving contact message to Supabase:", contactError);
// return {
// success: false,
// error: "Failed to save your message. Please try again later.",
// };
// }
// // Render admin email template
// const adminEmailHtml = await render(
// AdminNotification({
// name: formData.name,
// email: formData.email,
// phone: formData.phone,
// messageType: messageTypeLabel,
// message: formData.message,
// })
// );
// // Send email to admin
// const { data: emailData, error: emailError } = await resend.emails.send({
// from: "Contact Form <contact@backspacex.tech>",
// to: ["xdamazon17@gmail.com"],
// subject: `New Contact Form Submission: ${messageTypeLabel}`,
// html: adminEmailHtml,
// });
// if (emailError) {
// console.error("Error sending email via Resend:", emailError);
// // Note: We don't return error here since the data is already saved to Supabase
// }
// const userEmailHtml = await render(
// UserConfirmation({
// name: formData.name,
// messageType: messageTypeLabel,
// message: formData.message,
// })
// );
// // Send confirmation email to user
// const { data: confirmationData, error: confirmationError } =
// await resend.emails.send({
// from: "Your Company <support@backspacex.tech>",
// to: [formData.email],
// subject: "Thank you for contacting us",
// html: userEmailHtml,
// });
// if (confirmationError) {
// console.error("Error sending confirmation email:", confirmationError);
// // Note: We don't return error here either
// }
// return {
// success: true,
// message: "Your message has been sent successfully!",
// };
// } catch (error) {
// console.error("Unexpected error in sendContactEmail:", error);
// return {
// success: false,
// error: "An unexpected error occurred. Please try again later.",
// };
// }
// }

View File

@ -0,0 +1,37 @@
import { createClient } from "@/utils/supabase/server";
export const checkSession = async () => {
const supabase = await createClient();
try {
const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error) {
return {
success: false,
error: error.message,
};
}
if (session) {
return {
success: true,
session,
redirectTo: "/protected/dashboard", // or your preferred authenticated route
};
}
return {
success: false,
message: "No active session",
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
};
}
};

View File

@ -0,0 +1,54 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { redirect } from "next/navigation";
import { checkSession } from "./session";
export const signInAction = async (formData: FormData) => {
const supabase = await createClient();
const email = formData.get("email") as string;
const encodeEmail = encodeURIComponent(email);
try {
// First, check for existing session
const { session, error: sessionError } = await checkSession();
// If there's an active session and the email matches
if (session && session.user.email === email) {
return {
success: true,
message: "Already logged in",
redirectTo: "/protected/dashboard", // or wherever you want to redirect logged in users
};
}
// If no active session or different email, proceed with OTP
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
shouldCreateUser: false,
},
});
if (error) {
return {
success: false,
error: error.message,
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
}
return {
success: true,
message: "OTP has been sent to your email",
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
redirectTo: "/sign-in",
};
}
};

View File

@ -0,0 +1,10 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export const signOutAction = async () => {
const supabase = await createClient();
await supabase.auth.signOut();
return redirect("/sign-in");
};

View File

@ -0,0 +1,39 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { headers } from "next/headers";
export const signUpAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
if (!email || !password) {
return encodedRedirect(
"error",
"/sign-up",
"Email and password are required"
);
}
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});
if (error) {
console.error(error.code + " " + error.message);
return encodedRedirect("error", "/sign-up", error.message);
} else {
return encodedRedirect(
"success",
"/sign-up",
"Thanks for signing up! Please check your email for a verification link."
);
}
};

View File

@ -0,0 +1,30 @@
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export const verifyOtpAction = async (formData: FormData) => {
const email = formData.get("email") as string;
const token = formData.get("token") as string;
const supabase = await createClient();
console.log("email", email);
console.log("token", token);
if (!email || !token) {
redirect("/error?message=Email and OTP are required");
}
const {
data: { session },
error,
} = await supabase.auth.verifyOtp({
email,
token,
type: "email",
});
if (error) {
return redirect(`/verify-otp?error=${encodeURIComponent(error.message)}`);
}
return redirect("/protected/dashboard?message=OTP verified successfully");
};

View File

@ -0,0 +1,57 @@
"use server";
import { SignInFormData } from "@/src/models/auth/sign-in.model";
import { authRepository } from "@/src/repositories/authentication.repository";
export async function signIn(
data: SignInFormData
): Promise<{ success: boolean; message: string }> {
try {
await authRepository.signIn(data);
return { success: true, message: "Check your email for the login link!" };
} catch (error) {
console.error("Authentication error:", error);
return {
success: false,
message:
error instanceof Error
? error.message
: "Authentication failed. Please try again.",
};
}
}
export async function signOut(): Promise<{
success: boolean;
message: string;
}> {
try {
await authRepository.signOut();
return { success: true, message: "You have been signed out." };
} catch (error) {
console.error("Sign out error:", error);
return {
success: false,
message:
error instanceof Error
? error.message
: "Sign out failed. Please try again.",
};
}
}
export async function getUser() {
try {
const user = await authRepository.getUser();
return { success: true, user };
} catch (error) {
console.error("Get user error:", error);
return {
success: false,
message:
error instanceof Error
? error.message
: "Failed to get user information.",
};
}
}

View File

@ -0,0 +1,17 @@
import { redirect } from "next/navigation";
import { checkSession } from "./_actions/session";
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const sessionResult = await checkSession();
// If there's an active session, redirect to dashboard
if (sessionResult.success && sessionResult.redirectTo) {
redirect(sessionResult.redirectTo);
}
return <div className="max-w-full gap-12 items-start">{children}</div>;
}

View File

@ -1,8 +1,10 @@
import { ContactUsForm } from "@/app/(auth-pages)/_components/auth/contact-us";
import { Button } from "@/app/_components/ui/button";
import { SignInForm } from "@/components/auth/signin-form";
import { Message } from "@/components/form-message";
import { Button } from "@/components/ui/button";
import { GalleryVerticalEnd, Globe } from "lucide-react";
export default async function ContactAdminPage() {
export default async function Login(props: { searchParams: Promise<Message> }) {
const searchParams = await props.searchParams;
return (
<div className="grid min-h-svh lg:grid-cols-5">
<div className="flex flex-col gap-4 p-6 md:p-10 bg-[#171717] lg:col-span-2 relative border border-r-2 border-r-gray-400 border-opacity-20">
@ -16,7 +18,7 @@ export default async function ContactAdminPage() {
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-md">
<ContactUsForm />
<SignInForm />
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ArrowUpRight, InfoIcon } from "lucide-react";
import Link from "next/link";
export function SmtpMessage() {
return (
<div className="bg-muted/50 px-5 py-3 border rounded-md flex gap-4">
<InfoIcon size={16} className="mt-0.5" />
<div className="flex flex-col gap-1">
<small className="text-sm text-secondary-foreground">
<strong> Note:</strong> Emails are rate limited. Enable Custom SMTP to
increase the rate limit.
</small>
<div>
<Link
href="https://supabase.com/docs/guides/auth/auth-smtp"
target="_blank"
className="text-primary/50 hover:text-primary flex items-center text-sm gap-1"
>
Learn more <ArrowUpRight size={14} />
</Link>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
// import { GalleryVerticalEnd } from "lucide-react";
// export default async function VerifyOtpPage() {
// return (
// <div className="grid min-h-svh">
// <div className="flex flex-col gap-4 p-6 md:p-10 relative">
// <div className="flex justify-between items-center">
// <a href="#" className="flex items-center gap-2 font-medium">
// <div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
// <GalleryVerticalEnd className="size-4" />
// </div>
// Sigap Tech.
// </a>
// </div>
// <div className="flex flex-1 items-center justify-center">
// <div className="w-full max-w-lg">
// <VerifyOtpForm />
// </div>
// </div>
// </div>
// </div>
// );
// }

View File

@ -0,0 +1,24 @@
import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
// The `/auth/callback` route is required for the server-side auth flow implemented
// by the SSR package. It exchanges an auth code for the user's session.
// https://supabase.com/docs/guides/auth/server-side/nextjs
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get("code");
const origin = requestUrl.origin;
const redirectTo = requestUrl.searchParams.get("redirect_to")?.toString();
if (code) {
const supabase = await createClient();
await supabase.auth.exchangeCodeForSession(code);
}
if (redirectTo) {
return NextResponse.redirect(`${origin}${redirectTo}`);
}
// URL to redirect to after sign up process completes
return NextResponse.redirect(`${origin}/protected`);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,96 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 153 60% 53%; /* Supabase green */
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 153 60% 53%; /* Matching primary */
--radius: 0.5rem;
--chart-1: 153 60% 53%; /* Supabase green */
--chart-2: 183 65% 50%;
--chart-3: 213 70% 47%;
--chart-4: 243 75% 44%;
--chart-5: 273 80% 41%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 9%; /* #171717 */
--foreground: 210 20% 98%;
--card: 0 0% 9%; /* #171717 */
--card-foreground: 210 20% 98%;
--popover: 0 0% 9%; /* #171717 */
--popover-foreground: 210 20% 98%;
--primary: 153 60% 53%; /* Supabase green */
--primary-foreground: 210 20% 98%;
--secondary: 220 8% 15%;
--secondary-foreground: 210 20% 98%;
--muted: 220 8% 15%;
--muted-foreground: 217 10% 64%;
--accent: 220 8% 15%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 220 8% 15%;
--input: 220 8% 15%;
--ring: 153 60% 53%; /* Matching primary */
--chart-1: 153 60% 53%; /* Supabase green */
--chart-2: 183 65% 50%;
--chart-3: 213 70% 47%;
--chart-4: 243 75% 44%;
--chart-5: 273 80% 41%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -0,0 +1,86 @@
import DeployButton from "@/components/deploy-button";
import { EnvVarWarning } from "@/components/env-var-warning";
import HeaderAuth from "@/components/header-auth";
import { ThemeSwitcher } from "@/components/theme-switcher";
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
import { Geist } from "next/font/google";
import { ThemeProvider } from "next-themes";
import Link from "next/link";
import "./globals.css";
import ReactQueryProvider from "@/providers/react-query-provider";
import { Toaster } from "@/components/ui/sonner";
const defaultUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
export const metadata = {
metadataBase: new URL(defaultUrl),
title: "Next.js and Supabase Starter Kit",
description: "The fastest way to build apps with Next.js and Supabase",
};
const geistSans = Geist({
display: "swap",
subsets: ["latin"],
});
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={geistSans.className} suppressHydrationWarning>
<body className="bg-background text-foreground">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<ReactQueryProvider>
<main className="min-h-screen flex flex-col items-center">
<div className="flex-1 w-full gap-20 items-center">
{/* <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
<div className="w-full max-w-5xl flex justify-between items-center p-3 px-5 text-sm">
<div className="flex gap-5 items-center font-semibold">
<Link href={"/"}>
<SigapLogo />
</Link>
<div className="flex items-center gap-2">
<DeployButton />
</div>
</div>
<div className="flex gap-5 items-center">
{!hasEnvVars ? <EnvVarWarning /> : <HeaderAuth />}
<ThemeSwitcher />
</div>
</div>
</nav> */}
<div className="flex flex-col max-w-full p-0">
{children}
<Toaster />
</div>
{/* <footer className="w-full flex items-center justify-center border-t mx-auto text-center text-xs gap-8 py-16 mt-auto">
<p>
Powered by{" "}
<a
href=""
target="_blank"
className="font-bold hover:underline"
rel="noreferrer"
>
Politeknik Negeri Jember
</a>
</p>
</footer> */}
</div>
</main>
</ReactQueryProvider>
</ThemeProvider>
</body>
</html>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -0,0 +1,16 @@
import Hero from "@/components/hero";
import ConnectSupabaseSteps from "@/components/tutorial/connect-supabase-steps";
import SignUpUserSteps from "@/components/tutorial/sign-up-user-steps";
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
export default async function Home() {
return (
<>
<Hero />
<main className="flex-1 flex flex-col gap-6 px-4">
<h2 className="font-medium text-xl mb-4">Next steps</h2>
{hasEnvVars ? <SignUpUserSteps /> : <ConnectSupabaseSteps />}
</main>
</>
);
}

View File

@ -0,0 +1,38 @@
import FetchDataSteps from "@/components/tutorial/fetch-data-steps";
import { createClient } from "@/utils/supabase/server";
import { InfoIcon } from "lucide-react";
import { redirect } from "next/navigation";
export default async function ProtectedPage() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return redirect("/sign-in");
}
return (
<div className="flex-1 w-full flex flex-col gap-12">
<div className="w-full">
<div className="bg-accent text-sm p-3 px-5 rounded-md text-foreground flex gap-3 items-center">
<InfoIcon size="16" strokeWidth={2} />
This is a protected page that you can only see as an authenticated
user
</div>
</div>
<div className="flex flex-col gap-2 items-start">
<h2 className="font-bold text-2xl mb-4">Your user details</h2>
<pre className="text-xs font-mono p-3 rounded border max-h-32 overflow-auto">
{JSON.stringify(user, null, 2)}
</pre>
</div>
<div>
<h2 className="font-bold text-2xl mb-4">Next steps</h2>
<FetchDataSteps />
</div>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -1,16 +1,16 @@
"use client";
import { Button } from "@/app/_components/ui/button";
import { Input } from "@/app/_components/ui/input";
import type React from "react";
import { Github, Lock } from "lucide-react";
import { Lock } from "lucide-react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { SubmitButton } from "../submit-button";
import Link from "next/link";
import { SubmitButton } from "../../../_components/submit-button";
import { FormField } from "../form-field";
import { useSignInForm } from "@/src/controller/auth/sign-in-controller";
import { FormField } from "../../../_components/form-field";
import { useSignInForm } from "@/hooks/use-signin";
export function LoginForm2({
export function SignInForm({
className,
...props
}: React.ComponentPropsWithoutRef<"form">) {
@ -18,9 +18,8 @@ export function LoginForm2({
formData,
errors,
isSubmitting,
setFormData,
message,
handleChange,
handleSelectChange,
handleSubmit,
} = useSignInForm();
@ -35,15 +34,16 @@ export function LoginForm2({
<p className="text-sm text-gray-400">Sign in to your account</p>
</div>
<div className="space-y-4">
{/* <Button
variant="outline"
className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700"
size="lg"
{message && (
<div
className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative"
role="alert"
>
<Github className="mr-2 h-5 w-5" />
Continue with GitHub
</Button>*/}
<span className="block sm:inline">{message}</span>
</div>
)}
<div className="space-y-4">
<Button
variant="outline"
className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700"
@ -83,12 +83,6 @@ export function LoginForm2({
}
error={errors.email}
/>
{/* <Link
href="email-recovery"
className="flex items-end justify-end text-xs text-gray-400 hover:text-emerald-500"
>
Forgot Email?
</Link> */}
<SubmitButton
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
size="lg"

View File

@ -0,0 +1,96 @@
// "use client";
// const FormSchema = z.object({
// token: z.string().min(6, {
// message: "Your one-time password must be 6 characters.",
// }),
// });
// interface InputOTPFormProps {
// className?: string;
// [key: string]: any;
// }
// export function VerifyOtpForm({ className, ...props }: InputOTPFormProps) {
// const searchParams = useSearchParams();
// const email = searchParams.get("email") || "";
// const form = useForm<z.infer<typeof FormSchema>>({
// resolver: zodResolver(FormSchema),
// defaultValues: {
// token: "",
// },
// });
// async function onSubmit(data: z.infer<typeof FormSchema>) {
// try {
// } catch (error) {
// toast({
// variant: "destructive",
// title: "Error",
// description: "Failed to verify OTP. Please try again.",
// });
// }
// }
// return (
// <div className={cn("flex flex-col gap-6", className)} {...props}>
// <Card className="bg-[#171717] border-gray-800 text-white border-none">
// <CardHeader className="text-center">
// <CardTitle className="text-2xl font-bold">
// One-Time Password
// </CardTitle>
// <CardDescription className="text-gray-400">
// One time password is a security feature that helps protect your data
// </CardDescription>
// </CardHeader>
// <CardContent>
// <Form {...form}>
// <form className="space-y-6">
// <input type="hidden" name="email" value={email} />
// <FormField
// control={form.control}
// name="token"
// render={({ field }) => (
// <FormItem>
// <FormControl>
// <InputOTP maxLength={6} {...field}>
// <InputOTPGroup className="flex w-full items-center justify-center space-x-2">
// {[...Array(6)].map((_, index) => (
// <InputOTPSlot
// key={index}
// index={index}
// className="w-12 h-12 text-xl border-2 border-gray-700 bg-[#1C1C1C] text-white rounded-md focus:border-emerald-600 focus:ring-emerald-600"
// />
// ))}
// </InputOTPGroup>
// </InputOTP>
// </FormControl>
// <FormDescription className="flex w-full justify-center items-center text-gray-400">
// Please enter the one-time password sent to {email}.
// </FormDescription>
// <FormMessage className="text-red-400" />
// </FormItem>
// )}
// />
// <div className="flex justify-center">
// <SubmitButton
// className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
// pendingText="Verifying..."
// formAction={verifyOtpAction}
// >
// Submit
// </SubmitButton>
// </div>
// </form>
// </Form>
// </CardContent>
// </Card>
// <div className="text-balance text-center text-xs text-gray-400 [&_a]:text-emerald-500 [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-emerald-400">
// By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
// and <a href="#">Privacy Policy</a>.
// </div>
// </div>
// );
// }

View File

@ -1,9 +1,10 @@
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
import Link from "next/link";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { createClient } from "@/utils/supabase/server";
import { signOutAction } from "@/actions/auth/sign-out";
import { signOutAction } from "@/app/(auth-pages)/_actions/sign-out";
export default async function AuthButton() {
const supabase = await createClient();
@ -28,21 +29,21 @@ export default async function AuthButton() {
<Button
asChild
size="sm"
variant={"default"}
variant={"outline"}
disabled
className="opacity-75 cursor-none pointer-events-none"
>
<Link href="/sign-in">Sign in</Link>
</Button>
{/* <Button
<Button
asChild
size="sm"
variant={"outline"}
variant={"default"}
disabled
className="opacity-75 cursor-none pointer-events-none"
>
<Link href="/sign-up">Sign up</Link>
</Button> */}
</Button>
</div>
</div>
</>
@ -59,12 +60,12 @@ export default async function AuthButton() {
</div>
) : (
<div className="flex gap-2">
<Button asChild size="sm" variant={"default"}>
<Button asChild size="sm" variant={"outline"}>
<Link href="/sign-in">Sign in</Link>
</Button>
{/* <Button asChild size="sm" variant={"default"}>
<Button asChild size="sm" variant={"default"}>
<Link href="/sign-up">Sign up</Link>
</Button> */}
</Button>
</div>
);
}

View File

@ -1,5 +1,5 @@
import NextLogo from "./logo/next-logo";
import SupabaseLogo from "./logo/supabase-logo";
import NextLogo from "./next-logo";
import SupabaseLogo from "./supabase-logo";
export default function Header() {
return (

View File

@ -1,6 +1,6 @@
"use client";
import { Button } from "@/app/_components/ui/button";
import { Button } from "@/components/ui/button";
import { type ComponentProps } from "react";
import { useFormStatus } from "react-dom";

View File

@ -0,0 +1,78 @@
"use client";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Laptop, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
const ThemeSwitcher = () => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
const ICON_SIZE = 16;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size={"sm"}>
{theme === "light" ? (
<Sun
key="light"
size={ICON_SIZE}
className={"text-muted-foreground"}
/>
) : theme === "dark" ? (
<Moon
key="dark"
size={ICON_SIZE}
className={"text-muted-foreground"}
/>
) : (
<Laptop
key="system"
size={ICON_SIZE}
className={"text-muted-foreground"}
/>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-content" align="start">
<DropdownMenuRadioGroup
value={theme}
onValueChange={(e) => setTheme(e)}
>
<DropdownMenuRadioItem className="flex gap-2" value="light">
<Sun size={ICON_SIZE} className="text-muted-foreground" />{" "}
<span>Light</span>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem className="flex gap-2" value="dark">
<Moon size={ICON_SIZE} className="text-muted-foreground" />{" "}
<span>Dark</span>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem className="flex gap-2" value="system">
<Laptop size={ICON_SIZE} className="text-muted-foreground" />{" "}
<span>System</span>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
export { ThemeSwitcher };

View File

@ -16,7 +16,7 @@ values
const server = `import { createClient } from '@/utils/supabase/server'
export default async function Page() {
const supabase = createClient()
const supabase = await createClient()
const { data: notes } = await supabase.from('notes').select()
return <pre>{JSON.stringify(notes, null, 2)}</pre>

View File

@ -0,0 +1,56 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@ -0,0 +1,25 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@ -0,0 +1,26 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@ -6,7 +6,7 @@ import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/app/_components/ui/toast"
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000

View File

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

View File

@ -0,0 +1,20 @@
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - images - .svg, .png, .jpg, .jpeg, .gif, .webp
* Feel free to modify this pattern to include more paths.
*/
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};

View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

4358
sigap-website-v2/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"db:seed": "npx prisma db seed"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^6.4.1",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tabler/icons-react": "^3.30.0",
"@tanstack/react-query": "^5.66.9",
"autoprefixer": "10.4.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"next": "latest",
"next-themes": "^0.4.4",
"prettier": "^3.3.3",
"react": "19.0.0",
"react-dom": "19.0.0",
"sonner": "^2.0.1"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "19.0.2",
"postcss": "8.4.49",
"prisma": "^6.4.1",
"tailwind-merge": "^2.5.2",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,230 @@
const crimeCategories = [
{
name: "TERHADAP KETERTIBAN UMUM",
description:
"Tindak pidana yang mengganggu ketertiban dan kenyamanan masyarakat secara umum.",
},
{
name: "MEMBAHAYAKAN KAM UMUM",
description:
"Kejahatan yang berpotensi membahayakan keamanan dan keselamatan masyarakat.",
},
{
name: "PEMBAKARAN",
description:
"Tindakan membakar properti atau bangunan secara sengaja yang dapat membahayakan orang lain.",
},
{
name: "KEBAKARAN / MELETUS",
description:
"Kejadian kebakaran atau ledakan yang disebabkan oleh kelalaian atau tindakan kriminal.",
},
{
name: "MEMBER SUAP",
description:
"Pemberian sesuatu kepada pejabat untuk mempengaruhi keputusan atau tindakan tertentu.",
},
{
name: "SUMPAH PALSU",
description:
"Memberikan keterangan palsu di bawah sumpah, biasanya dalam proses hukum.",
},
{
name: "PEMALSUAN MATERAI",
description:
"Tindakan memalsukan materai resmi dengan tujuan merugikan pihak lain.",
},
{
name: "PEMALSUAN SURAT",
description:
"Membuat, mengubah, atau memalsukan surat yang memiliki kekuatan hukum.",
},
{
name: "PERZINAHAN",
description:
"Hubungan seksual di luar pernikahan yang melanggar norma hukum dan sosial.",
},
{
name: "PERKOSAAN",
description: "Tindak pemaksaan hubungan seksual tanpa persetujuan korban.",
},
{
name: "PERJUDIAN",
description:
"Segala bentuk kegiatan taruhan atau perjudian yang melanggar hukum.",
},
{
name: "PENGHINAAN",
description: "Ucapan atau tindakan yang merendahkan martabat seseorang.",
},
{
name: "PENCULIKAN",
description:
"Mengambil atau menahan seseorang secara paksa dengan tujuan tertentu.",
},
{
name: "PERBUATAN TIDAK MENYENANGKAN",
description:
"Tindakan yang mengakibatkan ketidaknyamanan atau kerugian emosional pada orang lain.",
},
{
name: "PEMBUNUHAN",
description: "Menghilangkan nyawa seseorang secara sengaja.",
},
{
name: "PENGANIAYAAN RINGAN",
description:
"Tindakan kekerasan yang mengakibatkan luka ringan pada korban.",
},
{
name: "PENGANIAYAAN BERAT",
description: "Kekerasan yang mengakibatkan luka serius atau kematian.",
},
{
name: "KELALAIAN AKIBATKAN ORANG MATI",
description: "Kelalaian yang menyebabkan kematian seseorang.",
},
{
name: "KELALAIAN AKIBATKAN ORANG LUKA",
description: "Kelalaian yang mengakibatkan luka pada orang lain.",
},
{
name: "PENCURIAN BIASA",
description: "Mengambil barang milik orang lain tanpa izin.",
},
{
name: "CURAT",
description:
"Pencurian dengan pemberatan, seperti pembobolan rumah atau kendaraan.",
},
{
name: "CURINGAN",
description: "Pencurian ringan dengan nilai kerugian yang kecil.",
},
{ name: "CURAS", description: "Pencurian dengan kekerasan terhadap korban." },
{ name: "CURANMOR", description: "Pencurian kendaraan bermotor." },
{
name: "PENGEROYOKAN",
description:
"Penyerangan secara bersama-sama terhadap satu atau beberapa orang.",
},
{
name: "PREMANISME",
description:
"Tindakan kekerasan, pemerasan, atau ancaman oleh kelompok tertentu untuk menguasai wilayah.",
},
{
name: "PEMERASAN DAN PENGANCAMAN",
description:
"Tindakan meminta sesuatu dengan ancaman kekerasan atau pengungkapan informasi merugikan.",
},
{
name: "PENGGELAPAN",
description:
"Mengambil barang atau uang yang dipercayakan untuk kepentingan pribadi.",
},
{
name: "PENIPUAN",
description:
"Menipu orang lain dengan tujuan mendapatkan keuntungan secara melawan hukum.",
},
{
name: "PENGRUSAKAN",
description: "Merusak properti orang lain secara sengaja.",
},
{
name: "KENAKALAN REMAJA",
description:
"Perilaku menyimpang oleh remaja yang dapat meresahkan masyarakat.",
},
{
name: "MENERIMA SUAP",
description:
"Menerima sesuatu dengan imbalan pengaruh keputusan atau tindakan tertentu.",
},
{
name: "PENADAHAN",
description: "Menyimpan atau menjual barang hasil kejahatan.",
},
{
name: "PEKERJAKAN ANAK",
description:
"Mempekerjakan anak di bawah umur dalam pekerjaan yang melanggar hukum.",
},
{
name: "AGRARIA",
description: "Kejahatan terkait sengketa tanah dan sumber daya agraria.",
},
{
name: "PERADILAN ANAK",
description: "Tindak pidana yang melibatkan anak dalam proses peradilan.",
},
{
name: "PERLINDUNGAN ANAK",
description:
"Kejahatan yang melanggar hak-hak anak dan kesejahteraan mereka.",
},
{
name: "PKDRT",
description:
"Tindak kekerasan dalam rumah tangga yang merugikan anggota keluarga.",
},
{
name: "PERLINDUNGAN TKI",
description:
"Kejahatan yang melibatkan pelanggaran terhadap hak Tenaga Kerja Indonesia di luar negeri.",
},
{
name: "PERLINDUNGAN SAKSI KORBAN",
description:
"Tindakan yang mengancam keselamatan saksi atau korban dalam proses hukum.",
},
{
name: "PTPPO",
description:
"Perdagangan orang, termasuk eksploitasi tenaga kerja dan seksual.",
},
{
name: "PORNOGRAFI",
description:
"Produksi, distribusi, atau konsumsi materi pornografi yang melanggar hukum.",
},
{
name: "SISTEM PERADILAN ANAK",
description:
"Pelaksanaan hukum dan keadilan yang berkaitan dengan anak sebagai pelaku kejahatan.",
},
{
name: "PENYELENGGARAN PEMILU",
description:
"Kejahatan yang mengganggu proses pemilihan umum, seperti kecurangan suara.",
},
{
name: "PEMERINTAH DAERAH",
description:
"Tindak pidana yang dilakukan oleh atau melibatkan aparat pemerintah daerah.",
},
{
name: "KEIMIGRASIAN",
description:
"Pelanggaran hukum yang terkait dengan masuk dan keluarnya orang dari suatu negara.",
},
{
name: "EKSTRADISI",
description:
"Proses penyerahan tersangka atau terpidana ke negara lain untuk diadili.",
},
{
name: "LAHGUN SENPI/HANDAK/SAJAM",
description:
"Penyalahgunaan senjata api, bahan peledak, atau senjata tajam.",
},
{
name: "PIDUM LAINNYA",
description:
"Tindak pidana umum lainnya yang tidak tercakup dalam kategori di atas.",
},
];
export default crimeCategories;

View File

@ -0,0 +1,294 @@
import {
IconHome,
IconAlertTriangle,
IconSettings,
IconMap,
IconDatabase,
IconUsers,
IconMessageCircle,
IconMenu2,
IconAlbum,
IconMusicBolt,
IconCommand,
IconFrame,
IconChartPie,
IconRobot,
IconSearch,
IconDashboard,
IconRobotFace,
IconGavel,
IconMapPin2,
IconSlice,
IconWorldBolt,
IconWorld,
IconPin,
IconMapPin,
IconLayersDifference,
IconFriends,
IconDna,
IconDna2,
IconUsersGroup,
IconNavigation,
IconApps,
} from "@tabler/icons-react";
export const navData = {
user: {
name: "user",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
teams: [
{
name: "Acme Inc",
icon: IconAlbum,
plan: "Enterprise",
},
{
name: "Acme Corp.",
icon: IconMusicBolt,
plan: "Startup",
},
{
name: "Evil Corp.",
icon: IconCommand,
plan: "Free",
},
],
NavPreMain: [
{
title: "Welcome",
url: "/protected/welcome",
icon: IconHome,
},
{
title: "Search",
url: "/search",
icon: IconSearch,
},
{
title: "Sigap AI",
url: "/protected/sigap-ai",
icon: IconRobotFace,
},
],
navMain: [
{
title: "Dashboard",
url: "/protected/dashboard",
slug: "dashboard",
orderSeq: 1,
icon: IconApps,
isActive: true,
subItems: [],
},
{
title: "Crime Management",
url: "/crime-management",
slug: "crime-management",
orderSeq: 2,
icon: IconGavel,
isActive: true,
subItems: [
{
title: "Crime Overview",
url: "/protected/crime-management/crime-overview",
slug: "crime-overview",
icon: IconMapPin2,
orderSeq: 1,
isActive: true,
},
{
title: "Crime Categories",
url: "/crime-management/crime-categories",
slug: "crime-categories",
icon: IconSlice,
orderSeq: 2,
isActive: true,
},
{
title: "Cases",
url: "/crime-management/crime-cases",
slug: "crime-cases",
icon: IconAlertTriangle,
orderSeq: 3,
isActive: true,
subSubItems: [
{
title: "New Case",
url: "/crime-management/crime-cases/case-new",
slug: "new-case",
icon: IconAlertTriangle,
orderSeq: 1,
isActive: true,
},
{
title: "Active Cases",
url: "/crime-management/crime-cases/case-active",
slug: "active-cases",
icon: IconAlertTriangle,
orderSeq: 2,
isActive: true,
},
{
title: "Resolved Cases",
url: "/crime-management/crime-cases/case-closed",
slug: "resolved-cases",
icon: IconAlertTriangle,
orderSeq: 3,
isActive: true,
},
],
},
],
},
{
title: "Geographic Data",
url: "/geographic-data",
slug: "geographic-data",
orderSeq: 3,
icon: IconWorld,
isActive: true,
subItems: [
{
title: "Locations",
url: "/geographic-data/locations",
slug: "locations",
icon: IconMapPin,
orderSeq: 1,
isActive: true,
subSubItems: [
{
title: "Cities",
url: "/geographic-data/cities",
slug: "cities",
icon: IconMap,
orderSeq: 1,
isActive: true,
},
{
title: "Districts",
url: "/geographic-data/districts",
slug: "districts",
icon: IconMap,
orderSeq: 2,
isActive: true,
},
],
},
{
title: "Geographic Info",
url: "/geographic-data/geographic-info",
slug: "geographic-info",
icon: IconLayersDifference,
orderSeq: 3,
isActive: true,
},
],
},
{
title: "Demographics",
url: "/demographics",
slug: "demographics",
orderSeq: 4,
icon: IconFriends,
isActive: true,
subItems: [
{
title: "Demographics Data",
url: "/demographics/demographics-data",
slug: "demographics-data",
icon: IconDna2,
orderSeq: 1,
isActive: true,
},
],
},
{
title: "User Management",
url: "/user-management",
slug: "user-management",
orderSeq: 5,
icon: IconUsers,
isActive: true,
subItems: [
{
title: "Users",
url: "/protected/user-management/users",
slug: "users",
icon: IconUsersGroup,
orderSeq: 1,
isActive: true,
},
],
},
// {
// title: "Communication",
// url: "/communication",
// slug: "communication",
// orderSeq: 6,
// icon: IconMessageCircle,
// isActive: true,
// subItems: [
// {
// title: "Contact Messages",
// url: "/communication/contact-messages",
// slug: "contact-messages",
// icon: IconMessageCircle,
// orderSeq: 1,
// isActive: true,
// },
// ],
// },
{
title: "Settings",
url: "/settings",
slug: "settings",
orderSeq: 6,
icon: IconSettings,
isActive: true,
subItems: [
{
title: "Navigation",
url: "/settings/navigation",
slug: "navigation",
icon: IconNavigation,
orderSeq: 1,
isActive: true,
subSubItems: [
{
title: "Nav Items",
url: "/settings/navigation/nav-items",
slug: "nav-items",
icon: IconMenu2,
orderSeq: 1,
isActive: true,
subSubItems: [
{
title: "Nav Sub Items",
url: "/settings/navigation/nav-sub-items",
slug: "nav-sub-items",
icon: IconMenu2,
orderSeq: 1,
isActive: true,
},
],
},
],
},
],
},
],
reports: [
{
name: "Crime Reports",
url: "#",
icon: IconFrame,
},
{
name: "Demographics Reports",
url: "#",
icon: IconChartPie,
},
],
};

View File

@ -0,0 +1,204 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
extensions = [pgcrypto, uuid_ossp(map: "uuid-ossp", schema: "extensions")]
}
model cities {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
geographic_id String? @db.Uuid
name String @db.VarChar(100)
code String @db.VarChar(10)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
geographics geographics? @relation(fields: [geographic_id], references: [id])
crimes crimes[]
demographics demographics[]
districts districts[]
@@index([name])
}
model contact_messages {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String? @db.VarChar(255)
email String? @db.VarChar(255)
phone String? @db.VarChar(20)
message_type String? @db.VarChar(50)
message_type_label String? @db.VarChar(50)
message String?
status status_contact_messages @default(new)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @db.Timestamptz(6)
}
model crime_cases {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
crime_id String? @db.Uuid
crime_category_id String? @db.Uuid
date DateTime @db.Timestamptz(6)
time DateTime @db.Timestamptz(6)
location String @db.VarChar(255)
latitude Float
longitude Float
description String
victim_count Int
status crime_status @default(new)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
crime_categories crime_categories? @relation(fields: [crime_category_id], references: [id])
crimes crimes? @relation(fields: [crime_id], references: [id])
}
model crime_categories {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @db.VarChar(255)
description String
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
crime_cases crime_cases[]
}
model crimes {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
district_id String? @db.Uuid
city_id String? @db.Uuid
year Int
number_of_crime Int
rate crime_rates @default(low)
heat_map Json?
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
crime_cases crime_cases[]
cities cities? @relation(fields: [city_id], references: [id])
districts districts? @relation(fields: [district_id], references: [id])
@@unique([city_id, year])
@@unique([district_id, year])
}
model demographics {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
district_id String? @db.Uuid
city_id String? @db.Uuid
province_id String? @db.Uuid
year Int
population Int
population_density Float
poverty_rate Float
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
cities cities? @relation(fields: [city_id], references: [id])
districts districts? @relation(fields: [district_id], references: [id])
@@unique([city_id, year])
@@unique([district_id, year])
}
model districts {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
city_id String @db.Uuid
name String @db.VarChar(100)
code String @db.VarChar(10)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
crimes crimes[]
demographics demographics[]
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade)
geographics geographics?
@@index([name])
}
model geographics {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
district_id String? @unique @db.Uuid
latitude Float?
longitude Float?
land_area Float?
polygon Json?
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
cities cities[]
districts districts? @relation(fields: [district_id], references: [id])
}
model profiles {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user_id String @unique @db.Uuid
bio String?
address String? @db.VarChar(255)
city String? @db.VarChar(100)
country String? @db.VarChar(100)
birth_date DateTime?
users users @relation(fields: [user_id], references: [id])
@@index([user_id])
}
model users {
id String @id @db.Uuid
email String @unique @db.VarChar(255)
email_verified Boolean @default(false)
first_name String? @db.VarChar(255)
last_name String? @db.VarChar(255)
avatar String? @db.VarChar(255)
role roles @default(user)
created_at DateTime @default(now())
updated_at DateTime
banned_until DateTime?
confirmation_sent_at DateTime?
confirmation_token String? @db.VarChar(255)
deleted_at DateTime?
email_change String? @db.VarChar(255)
email_change_sent_at DateTime?
email_change_token String? @db.VarChar(255)
email_confirmed_at DateTime?
encrypted_password String? @db.VarChar(255)
is_anonymous Boolean? @default(false)
is_sso_user Boolean? @default(false)
last_sign_in_at DateTime?
phone String? @db.VarChar(20)
phone_confirmed_at DateTime?
raw_app_meta_data Json?
raw_user_meta_data Json?
reauthentication_sent_at DateTime?
reauthentication_token String? @db.VarChar(255)
recovery_sent_at DateTime?
recovery_token String? @db.VarChar(255)
providers Json? @default("[]")
profiles profiles?
@@index([role])
}
enum crime_rates {
low
medium
high
}
enum crime_status {
new
in_progress
resolved
}
enum roles {
admin
staff
user
}
enum status_contact_messages {
new
read
replied
resolved
}

View File

@ -0,0 +1,14 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
export default ReactQueryProvider;

View File

@ -0,0 +1,85 @@
"use client";
import { signIn } from "@/app/(auth-pages)/action";
import {
defaultSignInValues,
SignInFormData,
signInSchema,
} from "@/src/models/auth/sign-in.model";
import { useState, type FormEvent, type ChangeEvent } from "react";
import { z } from "zod";
type SignInFormErrors = Partial<Record<keyof SignInFormData, string>>;
export function useSignInForm() {
const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues);
const [errors, setErrors] = useState<SignInFormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const validateForm = (): boolean => {
try {
signInSchema.parse(formData);
setErrors({});
return true;
} catch (error) {
if (error instanceof z.ZodError) {
const formattedErrors: SignInFormErrors = {};
error.errors.forEach((err) => {
const path = err.path[0] as keyof SignInFormData;
formattedErrors[path] = err.message;
});
setErrors(formattedErrors);
}
return false;
}
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setMessage(null);
try {
const result = await signIn(formData);
if (result.success) {
setMessage(result.message);
} else {
setErrors({
email: result.message || "Sign in failed. Please try again.",
});
}
} catch (error) {
console.error("Sign in failed", error);
setErrors({
email: "An unexpected error occurred. Please try again.",
});
} finally {
setIsSubmitting(false);
}
};
return {
formData,
errors,
isSubmitting,
message,
setFormData,
handleChange,
handleSubmit,
};
}

View File

@ -0,0 +1,17 @@
import { z } from "zod";
// Define the sign-in form schema using Zod
export const signInSchema = z.object({
email: z
.string()
.min(1, { message: "Email is required" })
.email({ message: "Invalid email address" }),
});
// Export the type derived from the schema
export type SignInFormData = z.infer<typeof signInSchema>;
// Default values for the form
export const defaultSignInValues: SignInFormData = {
email: "",
};

View File

@ -0,0 +1,13 @@
import { z } from "zod";
// Define the verify OTP form schema using Zod
export const verifyOtpSchema = z.object({
email: z
.string()
.min(1, { message: "Email is required" })
.email({ message: "Invalid email address" }),
token: z.string().min(6, { message: "OTP is required" }),
});
// Export the type derived from the schema
export type VerifyOtpFormData = z.infer<typeof verifyOtpSchema>;

View File

@ -0,0 +1,39 @@
import { createClient } from "@/utils/supabase/server";
import { SignInFormData } from "../models/auth/sign-in.model";
export class AuthRepository {
async signIn({ email }: SignInFormData) {
const supabase = await createClient();
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
},
});
if (error) {
throw new Error(error.message);
}
return data;
}
async signOut() {
const supabase = await createClient();
const { error } = await supabase.auth.signOut();
if (error) {
throw new Error(error.message);
}
}
async getUser() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
return user;
}
}
export const authRepository = new AuthRepository();

View File

@ -0,0 +1,80 @@
import type { Config } from "tailwindcss";
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
export default config;

View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,6 @@
// This check can be removed
// it is just for tutorial purposes
export const hasEnvVars =
process.env.NEXT_PUBLIC_SUPABASE_URL &&
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

View File

@ -0,0 +1,7 @@
import { createBrowserClient } from "@supabase/ssr";
export const createClient = () =>
createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

View File

@ -0,0 +1,62 @@
import { createServerClient } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
export const updateSession = async (request: NextRequest) => {
// This `try/catch` block is only here for the interactive tutorial.
// Feel free to remove once you have Supabase connected.
try {
// Create an unmodified response
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value),
);
response = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options),
);
},
},
},
);
// This will refresh session if expired - required for Server Components
// https://supabase.com/docs/guides/auth/server-side/nextjs
const user = await supabase.auth.getUser();
// protected routes
if (request.nextUrl.pathname.startsWith("/protected") && user.error) {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
if (request.nextUrl.pathname === "/" && !user.error) {
return NextResponse.redirect(new URL("/protected", request.url));
}
return response;
} catch (e) {
// If you are here, a Supabase client could not be created!
// This is likely because you have not set up environment variables.
// Check out http://localhost:3000 for Next Steps.
return NextResponse.next({
request: {
headers: request.headers,
},
});
}
};

View File

@ -0,0 +1,29 @@
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export const createClient = async () => {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
},
);
};

View File

@ -0,0 +1,16 @@
import { redirect } from "next/navigation";
/**
* Redirects to a specified path with an encoded message as a query parameter.
* @param {('error' | 'success')} type - The type of message, either 'error' or 'success'.
* @param {string} path - The path to redirect to.
* @param {string} message - The message to be encoded and added as a query parameter.
* @returns {never} This function doesn't return as it triggers a redirect.
*/
export function encodedRedirect(
type: "error" | "success",
path: string,
message: string,
) {
return redirect(`${path}?${type}=${encodeURIComponent(message)}`);
}

View File

@ -0,0 +1,4 @@
# Update these with your Supabase details from your project settings > API
# https://app.supabase.com/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

View File

@ -1,3 +0,0 @@
{
"recommendations": ["denoland.vscode-deno"]
}

View File

@ -1,22 +0,0 @@
{
"deno.enablePaths": ["supabase/functions"],
"deno.lint": true,
"deno.unstable": [
"bare-node-builtins",
"byonm",
"sloppy-imports",
"unsafe-proto",
"webgpu",
"broadcast-channel",
"worker-options",
"cron",
"kv",
"ffi",
"fs",
"http",
"net"
],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@ -0,0 +1,40 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const forgotPasswordAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
const callbackUrl = formData.get("callbackUrl")?.toString();
if (!email) {
return encodedRedirect("error", "/forgot-password", "Email is required");
}
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`,
});
if (error) {
console.error(error.message);
return encodedRedirect(
"error",
"/forgot-password",
"Could not reset password",
);
}
if (callbackUrl) {
return redirect(callbackUrl);
}
return encodedRedirect(
"success",
"/forgot-password",
"Check your email for a link to reset your password.",
);
};

View File

@ -0,0 +1,43 @@
"use server";
import { encodedRedirect } from "@/utils/utils";
import { createClient } from "@/utils/supabase/server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const resetPasswordAction = async (formData: FormData) => {
const supabase = await createClient();
const password = formData.get("password") as string;
const confirmPassword = formData.get("confirmPassword") as string;
if (!password || !confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password and confirm password are required"
);
}
if (password !== confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Passwords do not match"
);
}
const { error } = await supabase.auth.updateUser({
password: password,
});
if (error) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password update failed"
);
}
encodedRedirect("success", "/protected/reset-password", "Password updated");
};

View File

@ -0,0 +1,100 @@
// import { createClient } from "@/utils/supabase/server";
// export async function sendContactEmail(formData: {
// name: string;
// email: string;
// phone: string;
// typeMessage: string;
// message: string;
// }) {
// try {
// // Initialize Supabase
// const supabase = await createClient();
// const { resend } = useResend();
// // Get message type label
// const messageTypeLabel =
// typeMessageMap.get(formData.typeMessage) || "Unknown";
// // Save to Supabase
// const { data: contactData, error: contactError } = await supabase
// .from("contact_messages")
// .insert([
// {
// name: formData.name,
// email: formData.email,
// phone: formData.phone,
// message_type: formData.typeMessage,
// message_type_label: messageTypeLabel,
// message: formData.message,
// status: "new",
// },
// ])
// .select();
// if (contactError) {
// console.error("Error saving contact message to Supabase:", contactError);
// return {
// success: false,
// error: "Failed to save your message. Please try again later.",
// };
// }
// // Render admin email template
// const adminEmailHtml = await render(
// AdminNotification({
// name: formData.name,
// email: formData.email,
// phone: formData.phone,
// messageType: messageTypeLabel,
// message: formData.message,
// })
// );
// // Send email to admin
// const { data: emailData, error: emailError } = await resend.emails.send({
// from: "Contact Form <contact@backspacex.tech>",
// to: ["xdamazon17@gmail.com"],
// subject: `New Contact Form Submission: ${messageTypeLabel}`,
// html: adminEmailHtml,
// });
// if (emailError) {
// console.error("Error sending email via Resend:", emailError);
// // Note: We don't return error here since the data is already saved to Supabase
// }
// const userEmailHtml = await render(
// UserConfirmation({
// name: formData.name,
// messageType: messageTypeLabel,
// message: formData.message,
// })
// );
// // Send confirmation email to user
// const { data: confirmationData, error: confirmationError } =
// await resend.emails.send({
// from: "Your Company <support@backspacex.tech>",
// to: [formData.email],
// subject: "Thank you for contacting us",
// html: userEmailHtml,
// });
// if (confirmationError) {
// console.error("Error sending confirmation email:", confirmationError);
// // Note: We don't return error here either
// }
// return {
// success: true,
// message: "Your message has been sent successfully!",
// };
// } catch (error) {
// console.error("Unexpected error in sendContactEmail:", error);
// return {
// success: false,
// error: "An unexpected error occurred. Please try again later.",
// };
// }
// }

View File

@ -0,0 +1,37 @@
import { createClient } from "@/utils/supabase/server";
export const checkSession = async () => {
const supabase = await createClient();
try {
const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error) {
return {
success: false,
error: error.message,
};
}
if (session) {
return {
success: true,
session,
redirectTo: "/protected/dashboard", // or your preferred authenticated route
};
}
return {
success: false,
message: "No active session",
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
};
}
};

View File

@ -0,0 +1,54 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { redirect } from "next/navigation";
import { checkSession } from "./session";
export const signInAction = async (formData: FormData) => {
const supabase = await createClient();
const email = formData.get("email") as string;
const encodeEmail = encodeURIComponent(email);
try {
// First, check for existing session
const { session, error: sessionError } = await checkSession();
// If there's an active session and the email matches
if (session && session.user.email === email) {
return {
success: true,
message: "Already logged in",
redirectTo: "/protected/dashboard", // or wherever you want to redirect logged in users
};
}
// If no active session or different email, proceed with OTP
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
shouldCreateUser: false,
},
});
if (error) {
return {
success: false,
error: error.message,
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
}
return {
success: true,
message: "OTP has been sent to your email",
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
redirectTo: "/sign-in",
};
}
};

View File

@ -0,0 +1,10 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export const signOutAction = async () => {
const supabase = await createClient();
await supabase.auth.signOut();
return redirect("/sign-in");
};

View File

@ -0,0 +1,39 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { headers } from "next/headers";
export const signUpAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
if (!email || !password) {
return encodedRedirect(
"error",
"/sign-up",
"Email and password are required"
);
}
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});
if (error) {
console.error(error.code + " " + error.message);
return encodedRedirect("error", "/sign-up", error.message);
} else {
return encodedRedirect(
"success",
"/sign-up",
"Thanks for signing up! Please check your email for a verification link."
);
}
};

View File

@ -0,0 +1,30 @@
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export const verifyOtpAction = async (formData: FormData) => {
const email = formData.get("email") as string;
const token = formData.get("token") as string;
const supabase = await createClient();
console.log("email", email);
console.log("token", token);
if (!email || !token) {
redirect("/error?message=Email and OTP are required");
}
const {
data: { session },
error,
} = await supabase.auth.verifyOtp({
email,
token,
type: "email",
});
if (error) {
return redirect(`/verify-otp?error=${encodeURIComponent(error.message)}`);
}
return redirect("/protected/dashboard?message=OTP verified successfully");
};

View File

@ -1,167 +0,0 @@
"use client";
import * as React from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/app/_components/ui/card";
import { Input } from "@/app/_components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/app/_components/ui/select";
import { Textarea } from "../../../_components/ui/textarea";
import { SubmitButton } from "../../../_components/submit-button";
import Link from "next/link";
import { TValidator } from "@/utils/validator";
import { FormField } from "../../../_components/form-field";
import { typeMessage } from "@/src/entities/models/contact-us.model";
import { Form } from "../../../_components/ui/form";
import { useContactForm } from "@/hooks/use-contact-us-form";
export function ContactUsForm() {
const {
formData,
errors,
isSubmitting,
setFormData,
handleChange,
handleSelectChange,
handleSubmit,
} = useContactForm();
return (
<Card className="w-[500px] bg-[#171717] border-none text-white">
<CardHeader>
<CardTitle className="text-3xl font-bold">Contact Us</CardTitle>
<CardDescription className="text-gray-400">
Fill in the form below to contact the admin
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-2">
<FormField
label="Name"
input={
<Input
id="name"
placeholder="John doe"
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
errors.name ? "border-red-500" : ""
}`}
value={formData.name}
onChange={handleChange}
disabled={isSubmitting}
/>
}
error={errors.name}
/>
<FormField
label="Email"
input={
<Input
id="email"
placeholder="example@gmail.com"
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
errors.email ? "border-red-500" : ""
}`}
value={formData.email}
onChange={handleChange}
disabled={isSubmitting}
/>
}
error={errors.email}
/>
<FormField
label="Phone"
input={
<Input
id="phone"
placeholder="08123456789"
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
errors.phone ? "border-red-500" : ""
}`}
value={formData.phone}
onChange={handleChange}
disabled={isSubmitting}
/>
}
error={errors.phone}
/>
<FormField
label="Type message"
input={
<Select
value={formData.typeMessage}
onValueChange={handleSelectChange}
disabled={isSubmitting}
>
<SelectTrigger
id="typemessage"
className={`bg-[#1C1C1C] border-gray-800 text-white focus:border-emerald-600 focus:ring-emerald-600 ${
errors.typeMessage ? "border-red-500" : ""
}`}
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent className="bg-[#1C1C1C] border-gray-800 text-white">
{typeMessage.map((message) => (
<SelectItem
key={message.value}
value={message.value}
className="focus:bg-emerald-600 focus:text-white"
>
{message.label}
</SelectItem>
))}
</SelectContent>
</Select>
}
error={errors.typeMessage}
/>
<FormField
label="Message"
input={
<Textarea
id="message"
placeholder="Your message here..."
className={`resize-none h-24 bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
errors.message ? "border-red-500" : ""
}`}
value={formData.message}
onChange={handleChange}
disabled={isSubmitting}
/>
}
error={errors.message}
/>
<CardFooter className="flex flex-col items-center space-y-4 px-0">
<SubmitButton
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
disabled={isSubmitting}
pendingText="Sending..."
>
Send
</SubmitButton>
<div className="text-center text-lg space-x-2">
<span className="text-gray-400">Already have an account?</span>
<Link
href="/sign-in"
className="text-white hover:text-emerald-500"
>
Login
</Link>
</div>
</CardFooter>
</form>
</CardContent>
</Card>
);
}

View File

@ -1,110 +0,0 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "@/app/_hooks/use-toast";
import { Button } from "@/app/_components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/app/_components/ui/form";
import { Input } from "@/app/_components/ui/input";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/app/_components/ui/card";
import Link from "next/link";
import { SubmitButton } from "@/app/_components/submit-button";
const FormSchema = z.object({
email: z.string().email({
message: "Please enter a valid email address.",
}),
});
export default function RecoveryEmailForm() {
// const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
email: "",
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
setTimeout(() => {
toast({
title: "Recovery email sent",
description: `We've sent a recovery link to ${data.email}`,
});
// Redirect to a confirmation page or back to login
// router.push("/login");
}, 2000);
}
return (
<div className="flex items-center justify-center">
<Card className="w-[450px] text-white border-none">
<CardHeader>
<CardTitle className="text-2xl font-bold">Account Recovery</CardTitle>
<CardDescription className="text-gray-400">
Enter your email to receive a recovery link
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-200">Email</FormLabel>
<FormControl>
<Input
{...field}
type="email"
placeholder="you@example.com"
className="bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600"
/>
</FormControl>
<FormDescription className="text-gray-400">
We'll send a recovery link to this email
</FormDescription>
<FormMessage className="text-red-400" />
</FormItem>
)}
/>
<SubmitButton
pendingText="Sending..."
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
>
Send Recovery Link
</SubmitButton>
</form>
</Form>
</CardContent>
<CardFooter className="flex justify-center">
<Button
variant="link"
className="text-emerald-500 hover:text-emerald-400"
>
<Link href={"sign-in"}>Back to Login</Link>
</Button>
</CardFooter>
</Card>
</div>
);
}

View File

@ -1,103 +0,0 @@
// import { cn } from "@/lib/utils";
// import { Button } from "@/app/_components/ui/button";
// import {
// Card,
// CardContent,
// CardDescription,
// CardHeader,
// CardTitle,
// } from "@/app/_components/ui/card";
// import { Input } from "@/app/_components/ui/input";
// import { Label } from "@/components/ui/label";
// import { SubmitButton } from "../submit-button";
// import { signInAction } from "@/actions/auth/sign-in";
// export function LoginForm({
// className,
// ...props
// }: React.ComponentPropsWithoutRef<"div">) {
// return (
// <div className={cn("flex flex-col gap-6", className)} {...props}>
// <Card>
// <CardHeader className="text-center">
// <CardTitle className="text-xl">Welcome back</CardTitle>
// <CardDescription>
// Login with your Apple or Google account
// </CardDescription>
// </CardHeader>
// <CardContent>
// <form>
// <div className="grid gap-6">
// {/* <div className="flex flex-col gap-4">
// <Button variant="outline" className="w-full">
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
// <path
// d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
// fill="currentColor"
// />
// </svg>
// Login with Apple
// </Button>
// <Button variant="outline" className="w-full">
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
// <path
// d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
// fill="currentColor"
// />
// </svg>
// Login with Google
// </Button>
// </div> */}
// <div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
// {/* <span className="relative z-10 bg-background px-2 text-muted-foreground">
// Or continue with
// </span> */}
// </div>
// <div className="grid gap-6">
// <div className="grid gap-2">
// <Label htmlFor="email">Email</Label>
// <Input
// id="email"
// type="email"
// placeholder="m@example.com"
// required
// />
// </div>
// {/* <div className="grid gap-2">
// <div className="flex items-center">
// <Label htmlFor="password">Password</Label>
// <a
// href="#"
// className="ml-auto text-sm underline-offset-4 hover:underline"
// >
// Forgot your password?
// </a>
// </div>
// <Input id="password" type="password" required />
// </div> */}
// <SubmitButton
// type="submit"
// className="w-full"
// pendingText="Signing In..."
// formAction={signInAction}
// >
// Login
// </SubmitButton>
// </div>
// <div className="text-center text-sm">
// Don&apos;t have an account?{" "}
// <a href="/contact-us" className="underline underline-offset-4">
// Contact Us
// </a>
// </div>
// </div>
// </form>
// </CardContent>
// </Card>
// <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary ">
// By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
// and <a href="#">Privacy Policy</a>.
// </div>
// </div>
// );
// }

View File

@ -1,115 +0,0 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "@/app/_hooks/use-toast";
import { Button } from "@/app/_components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/app/_components/ui/form";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/app/_components/ui/input-otp";
import { SubmitButton } from "../../../_components/submit-button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../_components/ui/card";
import { cn } from "@/lib/utils";
const FormSchema = z.object({
pin: z.string().min(6, {
message: "Your one-time password must be 6 characters.",
}),
});
interface InputOTPFormProps {
className?: string;
[key: string]: any;
}
export function InputOTPForm({ className, ...props }: InputOTPFormProps) {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
pin: "",
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">One-Time Password</CardTitle>
<CardDescription>
One time password is a security feature that helps protect your data
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup className="flex w-full items-center justify-center space-x-2">
{[...Array(6)].map((_, index) => (
<InputOTPSlot
key={index}
index={index}
className="w-12 h-12 text-xl border-2 dark:border-gray-50/10 rounded-md"
/>
))}
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription className="flex w-full justify-center items-center">
Please enter the one-time password sent to your phone.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-center ">
<SubmitButton pendingText="verifying..." className="w-full">
Submit
</SubmitButton>
</div>
</form>
</Form>
</CardContent>
</Card>
<div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary ">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
);
}

View File

@ -0,0 +1,71 @@
// src/app/(auth-pages)/actions.ts
"use server";
import { SignInFormData } from "@/src/models/auth/sign-in.model";
import { VerifyOtpFormData } from "@/src/models/auth/verify-otp.model";
import { authRepository } from "@/src/repositories/authentication.repository";
import { redirect } from "next/navigation";
export async function signIn(
data: SignInFormData
): Promise<{ success: boolean; message: string; redirectTo?: string }> {
try {
const result = await authRepository.signIn(data);
return {
success: true,
message: "Check your email for the login link!",
redirectTo: result.redirectTo
};
} catch (error) {
console.error("Authentication error:", error);
return {
success: false,
message:
error instanceof Error
? error.message
: "Authentication failed. Please try again.",
};
}
}
export async function verifyOtp(
data: VerifyOtpFormData
): Promise<{ success: boolean; message: string; redirectTo?: string }> {
try {
const result = await authRepository.verifyOtp(data);
return {
success: true,
message: "Successfully authenticated!",
redirectTo: result.redirectTo
};
} catch (error) {
console.error("OTP verification error:", error);
return {
success: false,
message:
error instanceof Error
? error.message
: "OTP verification failed. Please try again.",
};
}
}
export async function signOut() {
try {
const result = await authRepository.signOut();
return {
success: true,
redirectTo: result.redirectTo
};
} catch (error) {
console.error("Sign out error:", error);
return {
success: false,
message:
error instanceof Error
? error.message
: "Sign out failed. Please try again.",
};
}
}

View File

@ -1,336 +0,0 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import AdminNotification from "../_components/email-templates/admin-notification";
import UserConfirmation from "../_components/email-templates/user-confirmation";
import { render } from "@react-email/components";
import { useResend } from "../_hooks/use-resend";
import { typeMessageMap } from "@/src/entities/models/contact-us.model";
export const signInAction = async (formData: { email: string }) => {
const supabase = await createClient();
const encodeEmail = encodeURIComponent(formData.email);
try {
// First, check for existing session
const {
data: { session },
error: sessionError,
} = await supabase.auth.getSession();
// If there's an active session and the email matches
if (session && session.user.email === formData.email) {
return {
success: true,
message: "Already logged in",
redirectTo: "/protected/dashboard", // or wherever you want to redirect logged in users
};
}
// If no active session or different email, proceed with OTP
const { data, error } = await supabase.auth.signInWithOtp({
email: formData.email,
options: {
shouldCreateUser: false,
},
});
if (error) {
return {
success: false,
error: error.message,
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
}
return {
success: true,
message: "OTP has been sent to your email",
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
redirectTo: "/sign-in",
};
}
};
export const signUpAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
if (!email || !password) {
return encodedRedirect(
"error",
"/sign-up",
"Email and password are required",
);
}
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});
if (error) {
console.error(error.code + " " + error.message);
return encodedRedirect("error", "/sign-up", error.message);
} else {
return encodedRedirect(
"success",
"/sign-up",
"Thanks for signing up! Please check your email for a verification link.",
);
}
};
export const checkSession = async () => {
const supabase = await createClient();
try {
const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error) {
return {
success: false,
error: error.message,
};
}
if (session) {
return {
success: true,
session,
redirectTo: "/protected/dashboard", // or your preferred authenticated route
};
}
return {
success: false,
message: "No active session",
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
};
}
};
export const forgotPasswordAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
const callbackUrl = formData.get("callbackUrl")?.toString();
if (!email) {
return encodedRedirect("error", "/forgot-password", "Email is required");
}
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`,
});
if (error) {
console.error(error.message);
return encodedRedirect(
"error",
"/forgot-password",
"Could not reset password"
);
}
if (callbackUrl) {
return redirect(callbackUrl);
}
return encodedRedirect(
"success",
"/forgot-password",
"Check your email for a link to reset your password."
);
};
export const resetPasswordAction = async (formData: FormData) => {
const supabase = await createClient();
const password = formData.get("password") as string;
const confirmPassword = formData.get("confirmPassword") as string;
if (!password || !confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password and confirm password are required"
);
}
if (password !== confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Passwords do not match"
);
}
const { error } = await supabase.auth.updateUser({
password: password,
});
if (error) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password update failed"
);
}
encodedRedirect("success", "/protected/reset-password", "Password updated");
};
export const verifyOtpAction = async (formData: FormData) => {
const email = formData.get("email") as string;
const token = formData.get("token") as string;
const supabase = await createClient();
console.log("email", email);
console.log("token", token);
if (!email || !token) {
redirect("/error?message=Email and OTP are required");
}
const {
data: { session },
error,
} = await supabase.auth.verifyOtp({
email,
token,
type: "email",
});
if (error) {
return redirect(`/verify-otp?error=${encodeURIComponent(error.message)}`);
}
return redirect("/protected/dashboard?message=OTP verified successfully");
};
export async function sendContactEmail(formData: {
name: string;
email: string;
phone: string;
typeMessage: string;
message: string;
}) {
try {
// Initialize Supabase
const supabase = await createClient();
const { resend } = useResend();
// Get message type label
const messageTypeLabel =
typeMessageMap.get(formData.typeMessage) || "Unknown";
// Save to Supabase
const { data: contactData, error: contactError } = await supabase
.from("contact_messages")
.insert([
{
name: formData.name,
email: formData.email,
phone: formData.phone,
message_type: formData.typeMessage,
message_type_label: messageTypeLabel,
message: formData.message,
status: "new",
},
])
.select();
if (contactError) {
console.error("Error saving contact message to Supabase:", contactError);
return {
success: false,
error: "Failed to save your message. Please try again later.",
};
}
// Render admin email template
const adminEmailHtml = await render(
AdminNotification({
name: formData.name,
email: formData.email,
phone: formData.phone,
messageType: messageTypeLabel,
message: formData.message,
})
);
// Send email to admin
const { data: emailData, error: emailError } = await resend.emails.send({
from: "Contact Form <contact@backspacex.tech>",
to: ["xdamazon17@gmail.com"],
subject: `New Contact Form Submission: ${messageTypeLabel}`,
html: adminEmailHtml,
});
if (emailError) {
console.error("Error sending email via Resend:", emailError);
// Note: We don't return error here since the data is already saved to Supabase
}
const userEmailHtml = await render(
UserConfirmation({
name: formData.name,
messageType: messageTypeLabel,
message: formData.message,
})
);
// Send confirmation email to user
const { data: confirmationData, error: confirmationError } =
await resend.emails.send({
from: "Your Company <support@backspacex.tech>",
to: [formData.email],
subject: "Thank you for contacting us",
html: userEmailHtml,
});
if (confirmationError) {
console.error("Error sending confirmation email:", confirmationError);
// Note: We don't return error here either
}
return {
success: true,
message: "Your message has been sent successfully!",
};
} catch (error) {
console.error("Unexpected error in sendContactEmail:", error);
return {
success: false,
error: "An unexpected error occurred. Please try again later.",
};
}
}
export const signOutAction = async () => {
const supabase = await createClient();
await supabase.auth.signOut();
return redirect("/sign-in");
};

View File

@ -1,24 +0,0 @@
import RecoveryEmailForm from "@/app/(auth-pages)/_components/auth/email-recovery";
import { GalleryVerticalEnd } from "lucide-react";
export default async function VerifyOtpPage() {
return (
<div className="grid min-h-svh">
<div className="flex flex-col gap-4 p-6 md:p-10 relative">
<div className="flex justify-between items-center">
<a href="#" className="flex items-center gap-2 font-medium">
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<GalleryVerticalEnd className="size-4" />
</div>
Sigap Tech.
</a>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-lg">
<RecoveryEmailForm />
</div>
</div>
</div>
</div>
);
}

View File

@ -1,39 +0,0 @@
import Link from "next/link";
import { SmtpMessage } from "../smtp-message";
import { Input } from "@/app/_components/ui/input";
import { Label } from "@/app/_components/ui/label";
import { SubmitButton } from "@/app/_components/submit-button";
import { FormMessage, Message } from "@/app/_components/form-message";
import { forgotPasswordAction } from "../actions";
export default async function ForgotPassword(props: {
searchParams: Promise<Message>;
}) {
const searchParams = await props.searchParams;
return (
<>
<form className="flex-1 flex flex-col w-full gap-2 text-foreground [&>input]:mb-6 min-w-64 max-w-64 mx-auto">
<div>
<h1 className="text-2xl font-medium">Reset Password</h1>
<p className="text-sm text-secondary-foreground">
Already have an account?{" "}
<Link className="text-primary underline" href="/sign-in">
Sign in
</Link>
</p>
</div>
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
<Label htmlFor="email">Email</Label>
<Input name="email" placeholder="you@example.com" required />
<SubmitButton formAction={forgotPasswordAction}>
Reset Password
</SubmitButton>
<FormMessage message={searchParams} />
</div>
</form>
<SmtpMessage />
</>
);
}

View File

@ -1,6 +1,5 @@
import { redirect } from "next/navigation";
import { checkSession } from "./actions";
import { checkSession } from "./_actions/session";
export default async function Layout({
children,

View File

@ -1,7 +1,7 @@
import { Message } from "@/app/_components/form-message";
import { Button } from "@/app/_components/ui/button";
import { SignInForm } from "@/components/auth/signin-form";
import { Message } from "@/components/form-message";
import { Button } from "@/components/ui/button";
import { GalleryVerticalEnd, Globe } from "lucide-react";
import { LoginForm2 } from "@/app/(auth-pages)/_components/auth/login-form-2";
export default async function Login(props: { searchParams: Promise<Message> }) {
const searchParams = await props.searchParams;
@ -18,7 +18,7 @@ export default async function Login(props: { searchParams: Promise<Message> }) {
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-md">
<LoginForm2 />
<SignInForm />
</div>
</div>
</div>

View File

@ -1,51 +0,0 @@
import { FormMessage, Message } from "@/app/_components/form-message";
import { SubmitButton } from "@/app/_components/submit-button";
import { Input } from "@/app/_components/ui/input";
import { Label } from "@/app/_components/ui/label";
import Link from "next/link";
import { SmtpMessage } from "../smtp-message";
import { signUpAction } from "@/actions/auth/sign-up-action";
export default async function SignupPage(props: {
searchParams: Promise<Message>;
}) {
const searchParams = await props.searchParams;
if ("message" in searchParams) {
return (
<div className="w-full flex-1 flex items-center h-screen sm:max-w-md justify-center gap-2 p-4">
<FormMessage message={searchParams} />
</div>
);
}
return (
<>
<form className="flex flex-col min-w-64 max-w-64 mx-auto">
<h1 className="text-2xl font-medium">Sign up</h1>
<p className="text-sm text text-foreground">
Already have an account?{" "}
<Link className="text-primary font-medium underline" href="/sign-in">
Sign in
</Link>
</p>
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
<Label htmlFor="email">Email</Label>
<Input name="email" placeholder="you@example.com" required />
<Label htmlFor="password">Password</Label>
<Input
type="password"
name="password"
placeholder="Your password"
minLength={6}
required
/>
<SubmitButton formAction={signUpAction} pendingText="Signing up...">
Sign up
</SubmitButton>
<FormMessage message={searchParams} />
</div>
</form>
<SmtpMessage />
</>
);
}

View File

@ -1,5 +1,5 @@
import { VerifyOtpForm } from "@/app/(auth-pages)/_components/auth/verify-otp-form";
import { VerifyOtpForm } from "@/components/auth/verify-otp-form";
import { GalleryVerticalEnd } from "lucide-react";
export default async function VerifyOtpPage() {

Some files were not shown because too many files have changed in this diff Show More