remove older project
This commit is contained in:
parent
10ce404a1d
commit
ca90871b22
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
|
||||
[](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)
|
|
@ -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.",
|
||||
);
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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.",
|
||||
// };
|
||||
// }
|
||||
// }
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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."
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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.",
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
// );
|
||||
// }
|
|
@ -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 |
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 |
|
@ -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"
|
|
@ -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>
|
||||
// );
|
||||
// }
|
|
@ -5,7 +5,7 @@ export type Message =
|
|||
|
||||
export function FormMessage({ message }: { message: Message }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full max-w-md text-sm ">
|
||||
<div className="flex flex-col gap-2 w-full max-w-md text-sm">
|
||||
{"success" in message && (
|
||||
<div className="text-foreground border-l-2 border-foreground px-4">
|
||||
{message.success}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 (
|
|
@ -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";
|
||||
|
|
@ -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 };
|
|
@ -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>
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 }
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
|
@ -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)$).*)",
|
||||
],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
|
@ -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;
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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: "",
|
||||
};
|
|
@ -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>;
|
|
@ -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();
|
|
@ -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;
|
|
@ -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"]
|
||||
}
|
|
@ -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;
|
|
@ -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!,
|
||||
);
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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.
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
|
@ -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)}`);
|
||||
}
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"recommendations": ["denoland.vscode-deno"]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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.",
|
||||
);
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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.",
|
||||
// };
|
||||
// }
|
||||
// }
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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."
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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'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>
|
||||
// );
|
||||
// }
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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.",
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
import { redirect } from "next/navigation";
|
||||
import { checkSession } from "./actions";
|
||||
import { checkSession } from "./_actions/session";
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue