diff --git a/.gitignore b/.gitignore index d32cc78..381079b 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + + +#sqlite +*.db \ No newline at end of file diff --git a/actions/auth-actions.ts b/actions/auth-actions.ts new file mode 100644 index 0000000..1b16f1f --- /dev/null +++ b/actions/auth-actions.ts @@ -0,0 +1,79 @@ +"use server"; + +import { authClient } from "@/lib/auth-client"; +import prisma from "@/lib/db"; +import type { signUpFormSchema } from "@/lib/schemas"; +import { redirect } from "next/navigation"; +import { z } from "zod"; + +const formSchema = z.object({ + phoneNumber: z + .string() + .regex(/^[7|9][0-9]{2}-[0-9]{4}$/, "Please enter a valid phone number"), +}); + +export async function signin( + currentState: { message: string; status: string }, + formData: FormData, +) { + const phoneNumber = formData.get("phoneNumber") as string; + const result = formSchema.safeParse({ phoneNumber }); + console.log(phoneNumber); + + if (!result.success) { + return { + message: result.error.errors[0].message, // Get the error message from Zod + status: "error", + }; + } + + if (!phoneNumber) { + return { + message: "Please enter a phone number", + status: "error", + }; + } + const NUMBER_WITH_COUNTRY_CODE: string = `+960${phoneNumber.split("-").join("")}`; + + const userExists = await prisma.user.findUnique({ + where: { + phoneNumber: NUMBER_WITH_COUNTRY_CODE, + }, + }); + if (!userExists) { + return redirect(`/signup?phone_number=${phoneNumber}`); + } + await authClient.phoneNumber.sendOtp({ + phoneNumber: NUMBER_WITH_COUNTRY_CODE, + }); + redirect("/verify-otp"); +} + +export async function signup({ + userData, +}: { userData: z.infer }) { + const newUser = await prisma.user.create({ + data: { ...userData, email: "" }, + }); + + redirect("/login"); +} + +export const sendOtp = async (phoneNumber: string, code: string) => { + // Implement sending OTP code via SMS + console.log("Send OTP server fn", phoneNumber, code); + const respose = await fetch("https://smsapi.sarlink.link/send", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api_key: process.env.SMS_API_KEY, + number: phoneNumber, + text: `Your OTP code is ${code}`, + }), + }); + const data = await respose.json(); + console.log(data); + return data; +}; diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..4f10988 --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,19 @@ +import LoginForm from "@/components/auth/login-form"; +import Image from "next/image"; +import React from "react"; + +export default function LoginPage() { + return
+
+ Sar Link Logo +
+ +

SAR Link Portal

+

Pay for your devices and track your bills.

+
+ +
+
; +} + + diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..ea634bd --- /dev/null +++ b/app/(auth)/signup/page.tsx @@ -0,0 +1,19 @@ +import SignUpForm from "@/components/auth/signup-form"; +import Image from "next/image"; +import React from "react"; + +export default function LoginPage() { + return
+
+ Sar Link Logo +
+ +

SAR Link Portal

+

Pay for your devices and track your bills.

+
+ +
+
; +} + + diff --git a/app/(auth)/verify-otp/page.tsx b/app/(auth)/verify-otp/page.tsx new file mode 100644 index 0000000..575cf75 --- /dev/null +++ b/app/(auth)/verify-otp/page.tsx @@ -0,0 +1,19 @@ +import VerifyOTPForm from "@/components/auth/verify-otp-form"; +import Image from "next/image"; +import React from "react"; + +export default function VerifyOTP() { + return
+
+ Sar Link Logo +
+ +

SAR Link Portal

+

Pay for your devices and track your bills.

+
+ +
+
; +} + + diff --git a/app/(dashboard)/devices/page.tsx b/app/(dashboard)/devices/page.tsx new file mode 100644 index 0000000..eeae826 --- /dev/null +++ b/app/(dashboard)/devices/page.tsx @@ -0,0 +1,5 @@ +export default async function Devices() { + return
+

Devices

+
; +} diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..dc3f380 --- /dev/null +++ b/app/(dashboard)/layout.tsx @@ -0,0 +1,9 @@ +import { ApplicationLayout } from "@/components/auth/application-layout"; + +export default function DashboardLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return {children}; +} diff --git a/app/(dashboard)/payments/page.tsx b/app/(dashboard)/payments/page.tsx new file mode 100644 index 0000000..90bbe8d --- /dev/null +++ b/app/(dashboard)/payments/page.tsx @@ -0,0 +1,17 @@ +'use client' +import { PhoneInput } from '@/components/ui/phone-input' +import React from 'react' + +export default function MyPayments() { + return ( +
+ +
+ + ) +} diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..e11351a --- /dev/null +++ b/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/lib/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { GET, POST } = toNextJsHandler(auth.handler); diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..34f1657 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css index 6b717ad..d4528b5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,20 +2,87 @@ @tailwind components; @tailwind utilities; -:root { - --background: #ffffff; - --foreground: #171717; +body { + font-family: Arial, Helvetica, sans-serif; } -@media (prefers-color-scheme: dark) { +@layer base { :root { - --background: #0a0a0a; - --foreground: #ededed; + --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: 240 5.9% 10%; + --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: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --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: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --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%; } } -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } diff --git a/app/layout.tsx b/app/layout.tsx index a36cde0..5358e7c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,35 +1,40 @@ import type { Metadata } from "next"; -import localFont from "next/font/local"; import "./globals.css"; +import { ThemeProvider } from "@/components/theme-provider"; +import { Barlow } from "next/font/google"; +import NextTopLoader from 'nextjs-toploader'; +import { Toaster } from 'sonner' -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", - weight: "100 900", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", - weight: "100 900", +const barlow = Barlow({ + subsets: ["latin"], + weight: ["100", "300", "400", "500", "600", "700", "800", "900"], + variable: "--font-barlow", }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Create Next App", + description: "Generated by create next app", }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - {children} - - - ); + return ( + + + + + + {children} + + + + ); } diff --git a/app/page.tsx b/app/page.tsx index 9007252..a8d17e8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,101 +1,5 @@ -import Image from "next/image"; +import { redirect } from "next/navigation"; -export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
- - -
- -
- ); +export default async function Home() { + return redirect("/devices"); } diff --git a/components.json b/components.json new file mode 100644 index 0000000..a312865 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/auth/account-popver.tsx b/components/auth/account-popver.tsx new file mode 100644 index 0000000..9991782 --- /dev/null +++ b/components/auth/account-popver.tsx @@ -0,0 +1,49 @@ +'use client' +import { Button } from "@/components/ui/button" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { authClient } from "@/lib/auth-client" +import type { User } from "better-auth" +import { Loader, User as UserIcon } from "lucide-react" +import { useRouter } from "next/navigation" +import { useState } from "react" + +export function AccountPopover({ user }: { user?: User }) { + const [loading, setLoading] = useState(false) + const router = useRouter() + return ( + + + + + +
+
+

{user?.name}

+

+ {user?.email} +

+
+ +
+
+
+ ) +} diff --git a/components/auth/application-layout.tsx b/components/auth/application-layout.tsx new file mode 100644 index 0000000..4d66be2 --- /dev/null +++ b/components/auth/application-layout.tsx @@ -0,0 +1,60 @@ +import { ModeToggle } from "@/components/theme-toggle"; +import { AppSidebar } from "@/components/ui/app-sidebar"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Separator } from "@/components/ui/separator"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { AccountPopover } from "./account-popver"; + +export async function ApplicationLayout({ children }: { children: React.ReactNode }) { + const session = await auth.api.getSession({ + headers: await headers(), // you need to pass the headers object. + }); + return ( + + + +
+
+ + + + + + + MENU + + + + + Sar Link Portal v1.0.0 + + + +
+ +
+ + +
+
+
+ + {children} +
+
+
+ ); +} diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx new file mode 100644 index 0000000..dfe4b19 --- /dev/null +++ b/components/auth/login-form.tsx @@ -0,0 +1,47 @@ +"use client"; + + +import { Button } from "@/components/ui/button"; + +import { signin } from "@/actions/auth-actions"; +import { Loader } from "lucide-react"; +import Link from "next/link"; +import { useActionState } from "react"; +import { PhoneInput } from "../ui/phone-input"; + +export default function LoginForm() { + const [state, formAction, isPending] = useActionState(signin, { + message: "", + status: "", + }); + + return ( +
+

Login

+
+
+ + + {state.status === "error" && ( +

{state.message}

+ )} + +
+
+ Don't have an account?{" "} + + Sign up + +
+
+
+ ); +} diff --git a/components/auth/signup-form.tsx b/components/auth/signup-form.tsx new file mode 100644 index 0000000..12bcfe2 --- /dev/null +++ b/components/auth/signup-form.tsx @@ -0,0 +1,167 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { signUpFormSchema } from "@/lib/schemas"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; + +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import type { z } from "zod"; +import { DatePicker } from "../ui/datepicker"; + + +export default function SignUpForm() { + const params = useSearchParams(); + console.log(params); + const phone_number = params.get("phone_number"); + const form = useForm>({ + resolver: zodResolver(signUpFormSchema), + defaultValues: { + phoneNumber: phone_number ?? "", + name: "", + id_card: "", + house_name: "", + island: "F.Dharanboodhoo", + }, + }); + + function onSubmit(values: z.infer) { + console.log(values); + } + + return ( +
+ +

+ Sign up +

+
+ ( + + Name + + + + + + )} + /> + ( + + ID Card + + + + + + )} + /> + ( + + Island + + + + + + )} + /> + ( + + House Name + + + + + + )} + /> + ( + + Date of birth + + + + + + )} + /> + ( + + Phone number + +
+
+ + + +
+ +
+
+ +
+ )} + /> + +
+
+ Already have an account?{" "} + + login + +
+ +
+ + ); +} diff --git a/components/auth/verify-otp-form.tsx b/components/auth/verify-otp-form.tsx new file mode 100644 index 0000000..4bb3df1 --- /dev/null +++ b/components/auth/verify-otp-form.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { authClient } from "@/lib/auth-client"; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Loader } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useTransition } from "react"; +import { type SubmitHandler, useForm } from "react-hook-form"; +import { toast } from 'sonner' +import { z } from "zod"; +const OTPSchema = z.object({ + pin: z.string().min(6, { + message: "Your one-time password must be 6 characters.", + + }), +}); + +export default function VerifyOTPForm() { + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm>({ + defaultValues: { + pin: "", + }, + resolver: zodResolver(OTPSchema), + }); + + const onSubmit: SubmitHandler> = (data) => { + console.log(data); + + startTransition(async () => { + const isVerified = await authClient.phoneNumber.verify({ + phoneNumber: "+9607780588", + code: data.pin, + }); + if (!isVerified.error) { + router.push("/devices"); + } else { + toast.error(isVerified.error.message); + } + }); + } + + return ( +
+

Verify OTP

+
+
+ + + {errors.pin && ( +

{errors.pin.message}

+ )} +
+ +
+
+ Go back to{" "} + + login + +
+
+ ); +} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..628df08 --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { ThemeProvider as NextThemesProvider } from "next-themes"; + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children}; +} diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx new file mode 100644 index 0000000..3b027a0 --- /dev/null +++ b/components/theme-toggle.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/components/ui/app-sidebar.tsx b/components/ui/app-sidebar.tsx new file mode 100644 index 0000000..205d5ec --- /dev/null +++ b/components/ui/app-sidebar.tsx @@ -0,0 +1,91 @@ +import { ChevronRight } from "lucide-react"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, +} from "@/components/ui/sidebar"; +import Link from "next/link"; + +const data = { + navMain: [ + { + title: "MENU", + url: "#", + items: [ + { + title: "Devices", + url: "/devices", + }, + { + title: "Payments", + url: "/payments", + }, + ], + }, + + ], +}; + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + +

+ Sar Link Portal +

+
+ + {/* We create a collapsible SidebarGroup for each parent. */} + {data.navMain.map((item) => ( + + + + + {item.title}{" "} + + + + + + + {item.items.map((item) => ( + + + + {item.title} + + + + ))} + + + + + + ))} + + +
+ ); +} diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>