diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dc0218b --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +MONGO_URI= + +UPLOADTHING_SECRET= +UPLOADTHING_APP_ID= + +ADMIN_USERNAME= +ADMIN_PASSWORD= \ No newline at end of file diff --git a/actions/login.action.ts b/actions/login.action.ts new file mode 100644 index 0000000..ffa2912 --- /dev/null +++ b/actions/login.action.ts @@ -0,0 +1,9 @@ +"use server"; + +export function CheckLoggedIn(username: string, password: string) { + if ( + username === process.env.ADMIN_USERNAME && + password === process.env.ADMIN_PASSWORD + ) { + } +} diff --git a/app/(auth)/_components/Card.tsx b/app/(auth)/_components/Card.tsx new file mode 100644 index 0000000..b603c42 --- /dev/null +++ b/app/(auth)/_components/Card.tsx @@ -0,0 +1,23 @@ +import { ArrowUpRight } from "lucide-react"; +import Link from "next/link"; +import { FC } from "react"; + +interface CardProps { + title: string; + url: string; +} + +const Card: FC = ({ title, url }) => { + return ( +
+ +
+

{title}

+ +
+ +
+ ); +}; + +export default Card; diff --git a/app/(auth)/_components/Mobilenav.tsx b/app/(auth)/_components/Mobilenav.tsx new file mode 100644 index 0000000..7c901c3 --- /dev/null +++ b/app/(auth)/_components/Mobilenav.tsx @@ -0,0 +1,56 @@ +"use client"; +import React from "react"; +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import Link from "next/link"; +import Image from "next/image"; +import { AdminLinks } from "@/constants"; +import { usePathname } from "next/navigation"; +import { Menu } from "lucide-react"; + +const MobileNav = () => { + const pathname = usePathname(); + return ( +
+ + + + + +
+ ); +}; + +export default MobileNav; diff --git a/app/(auth)/_components/Sidebar.tsx b/app/(auth)/_components/Sidebar.tsx new file mode 100644 index 0000000..74bab14 --- /dev/null +++ b/app/(auth)/_components/Sidebar.tsx @@ -0,0 +1,65 @@ +"use client"; + +import Link from "next/link"; +import Image from "next/image"; +import { usePathname } from "next/navigation"; +import { AdminLinks } from "@/constants"; + +const Sidebar = () => { + const pathname = usePathname(); + return ( + <> + + + ); +}; + +export default Sidebar; diff --git a/app/(auth)/admin/downloads/page.tsx b/app/(auth)/admin/downloads/page.tsx new file mode 100644 index 0000000..286cfaf --- /dev/null +++ b/app/(auth)/admin/downloads/page.tsx @@ -0,0 +1,125 @@ +"use client"; + +import React from "react"; +import { useForm, SubmitHandler } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { UploadButton, UploadDropzone } from "@/lib/uploadthing"; +import { downloadSchema } from "@/lib/validations/validation"; + +const AdminPage = () => { + const form = useForm>({ + resolver: zodResolver(downloadSchema), + }); + + const onSubmit: SubmitHandler> = async ( + data + ) => { + const response = await fetch("/api/upload", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + if (response.ok) { + form.reset(); + console.log("Upload successful"); + alert("Uploaded"); + } else { + console.error("Upload failed"); + alert("Upload Failed"); + } + }; + + return ( +
+

Admin Upload Section

+
+ + ( + + File Name + + + + + + )} + /> + ( + + Version + + + + + + )} + /> + ( + + Download Link + { + field.onChange(res[0].url); + }} + onUploadError={(error: Error) => { + alert(`ERROR! ${error.message}`); + }} + /> + + + + + + )} + /> + ( + + File Size + + + + + + )} + /> + + + +
+ ); +}; + +export default AdminPage; diff --git a/app/(auth)/admin/page.tsx b/app/(auth)/admin/page.tsx new file mode 100644 index 0000000..0ede98b --- /dev/null +++ b/app/(auth)/admin/page.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import Card from "../_components/Card"; + +const AdminPage = () => { + return ( + <> +
+

Admin Page

+ +
+ + + + +
+
+ + ); +}; + +export default AdminPage; diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..b6569b8 --- /dev/null +++ b/app/(auth)/layout.tsx @@ -0,0 +1,16 @@ +import MobileNav from "./_components/Mobilenav"; +import Sidebar from "./_components/Sidebar"; + +export default function PageLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ + +
{children}
+
+ ); +} diff --git a/app/(root)/downloads/page.tsx b/app/(root)/downloads/page.tsx index 700d84e..f06d56a 100644 --- a/app/(root)/downloads/page.tsx +++ b/app/(root)/downloads/page.tsx @@ -1,10 +1,12 @@ -import { Button } from '@/components/ui/button'; +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCaption, TableCell, - TableFooter, TableHead, TableHeader, TableRow, @@ -12,38 +14,45 @@ import { import { Download } from 'lucide-react'; import Link from 'next/link'; -const downloads = [ - { - date: '2024-06-01', - fileName: 'SVRJS_v1.0.0.zip', - version: '1.0.0', - fileSize: '15MB', - downloadLink: '/downloads/SVRJS_v1.0.0.zip', - }, - { - date: '2024-06-10', - fileName: 'SVRJS_v1.1.0.zip', - version: '1.1.0', - fileSize: '18MB', - downloadLink: '/downloads/SVRJS_v1.1.0.zip', - }, - { - date: '2024-06-15', - fileName: 'SVRJS_v1.2.0.zip', - version: '1.2.0', - fileSize: '20MB', - downloadLink: '/downloads/SVRJS_v1.2.0.zip', - }, - { - date: '2024-06-20', - fileName: 'SVRJS_v1.3.0.zip', - version: '1.3.0', - fileSize: '22MB', - downloadLink: '/downloads/SVRJS_v1.3.0.zip', - }, -]; +interface Download { + _id: string; + date: string; + fileName: string; + version: string; + fileSize: string; + downloadLink: string; +} + +const DownloadPage: React.FC = () => { + const [downloads, setDownloads] = useState([]); + const [error, setError] = useState(""); + + const fetchDownloads = async () => { + try { + const response = await fetch("/api/downloads", { + method: "GET", + }); + if (response.ok) { + const data: Download[] = await response.json(); + setDownloads(data); + } else { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error: any) { + setError(error); + } + }; + + useEffect(() => { + fetchDownloads(); + + const interval = setInterval(() => { + fetchDownloads(); + }, 10000); + + return () => clearInterval(interval); + }, []); -const DownloadPage = () => { return (
{

Get all the latest version of SVRJS download and compiled Files here!

+ {error &&

{error}

} A list of all available downloads. @@ -62,23 +72,23 @@ const DownloadPage = () => { Date File Name Version - Download Link - File Size + File Size + Download Link {downloads - .slice() + .slice(0, 10) .reverse() .map((download) => ( - + {download.date} {download.fileName} {download.version} {download.fileSize} - diff --git a/app/(root)/layout.tsx b/app/(root)/layout.tsx new file mode 100644 index 0000000..1776783 --- /dev/null +++ b/app/(root)/layout.tsx @@ -0,0 +1,16 @@ +import Footer from "@/components/shared/Footer"; +import Navbar from "@/components/shared/Navbar"; + +export default function PageLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ +
{children}
+
+
+ ); +} diff --git a/app/page.tsx b/app/(root)/page.tsx similarity index 100% rename from app/page.tsx rename to app/(root)/page.tsx diff --git a/app/api/downloads/route.ts b/app/api/downloads/route.ts new file mode 100644 index 0000000..23a04df --- /dev/null +++ b/app/api/downloads/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from "next/server"; +import clientPromise from "@/lib/db"; + +// Handler for GET requests +export async function GET(req: NextRequest) { + try { + const client = await clientPromise; + const db = client.db("downloadsDatabase"); + const downloads = await db.collection("downloads").find().toArray(); + return NextResponse.json(downloads, { status: 200 }); + } catch (error) { + return NextResponse.json( + { error: "Failed to fetch downloads" }, + { status: 500 } + ); + } +} diff --git a/app/api/login/route.ts b/app/api/login/route.ts index b731755..0e7583e 100644 --- a/app/api/login/route.ts +++ b/app/api/login/route.ts @@ -1,31 +1,31 @@ -// import { NextResponse } from 'next/server'; -// import { setCookie } from 'nookies'; -// require('dotenv').config(); +import { NextRequest, NextResponse } from "next/server"; +import { serialize } from "cookie"; -// // nvm, clerk is overkill for one u +export async function POST(request: NextRequest) { + const { username, password } = await request.json(); -// export async function POST(request: NextApiRequest) { -// const { username, password } = await request.json(); -// console.log(username, password); -// console.log(process.env.PASSWORD); + const adminUsername = process.env.ADMIN_USERNAME; + const adminPassword = process.env.ADMIN_PASSWORD; -// if (username === process.env.USERNAME && password === process.env.PASSWORD) { -// const response = NextResponse.json( -// { message: 'Login successful' }, -// { status: 200 } -// ); + if (username === adminUsername && password === adminPassword) { + const cookie = serialize("auth", "authenticated", { + httpOnly: true, + path: "/", + maxAge: 60 * 60 * 24, // 1 day + }); -// setCookie({ res: response }, 'token', 'your-auth-token', { -// httpOnly: true, -// secure: process.env.NODE_ENV !== 'development', -// maxAge: 30 * 24 * 60 * 60, -// path: '/', -// }); + return new NextResponse(JSON.stringify({ message: "Login successful" }), { + headers: { + "Set-Cookie": cookie, + "Content-Type": "application/json", + }, + }); + } -// return response; -// } else { -// return NextResponse.json({ message: 'Login failed' }, { status: 401 }); -// } -// } - -// im gonna create server actions + return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), { + status: 401, + headers: { + "Content-Type": "application/json", + }, + }); +} diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts new file mode 100644 index 0000000..abe2283 --- /dev/null +++ b/app/api/upload/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; +import clientPromise from "@/lib/db"; + +export async function POST(request: Request) { + const body = await request.json(); + const { fileName, version, downloadLink, fileSize } = body; + + const client = await clientPromise; + const db = client.db("downloadsDatabase"); + + const result = await db.collection("downloads").insertOne({ + date: new Date().toISOString().split("T")[0], + fileName, + version, + downloadLink, + fileSize, + }); + + return NextResponse.json({ success: true, id: result.insertedId }); +} diff --git a/app/api/uploadthing/core.ts b/app/api/uploadthing/core.ts new file mode 100644 index 0000000..3a26bf3 --- /dev/null +++ b/app/api/uploadthing/core.ts @@ -0,0 +1,22 @@ +import { createUploadthing, type FileRouter } from "uploadthing/next"; +import { UploadThingError } from "uploadthing/server"; + +const f = createUploadthing(); + +// const auth = (req: Request) => ({ id: "fakeId" }); + +export const ourFileRouter = { + imageUploader: f({ "application/zip": { maxFileSize: "8MB" } }) + // .middleware(async ({ req }) => { + // const user = await auth(req); + // if (!user) throw new UploadThingError("Unauthorized"); + // return { userId: user.id }; + // }) + .onUploadComplete(async ({ metadata, file }) => { + // console.log("Upload complete for userId:", metadata.userId); + console.log("file url", file.url); + // return { uploadedBy: metadata.userId }; + }), +} satisfies FileRouter; + +export type OurFileRouter = typeof ourFileRouter; diff --git a/app/api/uploadthing/route.ts b/app/api/uploadthing/route.ts new file mode 100644 index 0000000..f8f1912 --- /dev/null +++ b/app/api/uploadthing/route.ts @@ -0,0 +1,6 @@ +import { createRouteHandler } from "uploadthing/next"; +import { ourFileRouter } from "./core"; + +export const { GET, POST } = createRouteHandler({ + router: ourFileRouter, +}); diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..b511e44 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css index 7d422ca..c0ba6ac 100644 --- a/app/globals.css +++ b/app/globals.css @@ -99,6 +99,54 @@ body { .flex-end { @apply flex justify-between items-end; } + .root { + @apply flex max-h-screen w-full flex-col lg:flex-row; + } + .root-container { + @apply mt-16 flex-1 overflow-auto py-8 lg:mt-0 lg:max-h-screen lg:py-10; + } + + /* .gradient-text { + @apply bg-purple-gradient bg-cover bg-clip-text text-transparent; + } */ + .sheet-content button { + @apply focus:ring-0 focus-visible:ring-transparent focus:ring-offset-0 focus-visible:ring-offset-0 focus-visible:outline-none focus-visible:border-none !important; + } + + .sidebar { + @apply hidden h-screen w-72 p-5 shadow-md shadow-purple-200/50 lg:flex; + } + .header { + @apply flex-between fixed h-16 w-full border-b p-5 lg:hidden; + } + + .header-nav_elements { + @apply mt-8 flex w-full flex-col items-start gap-5; + } + + /* Search Component */ + .search { + @apply flex w-full rounded-[16px] border-2 border-purple-200/20 bg-white px-4 shadow-sm shadow-purple-200/15 md:max-w-96; + } + .sidebar-logo { + @apply flex items-center gap-2 md:py-2; + } + + .sidebar-nav { + @apply h-full flex-col justify-between md:flex md:gap-4; + } + + .sidebar-nav_elements { + @apply hidden w-full flex-col items-start gap-2 md:flex; + } + + .sidebar-nav_element { + @apply flex-center p-medium-16 w-full whitespace-nowrap rounded-full bg-cover transition-all hover:bg-white/10 hover:shadow-inner; + } + + .sidebar-link { + @apply flex p-medium-16 size-full gap-4 p-4; + } /* TYPOGRAPHY */ /* 64 */ diff --git a/app/layout.tsx b/app/layout.tsx index 461766d..b8d2da8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,7 @@ -import type { Metadata } from 'next'; -import { Poppins } from 'next/font/google'; -import './globals.css'; -import { ThemeProvider } from '@/components/shared/providers/themeprovider'; -import Navbar from '@/components/shared/Navbar'; -import Footer from '@/components/shared/Footer'; +import type { Metadata } from "next"; +import { Poppins } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "@/components/shared/providers/themeprovider"; const poppins = Poppins({ weight: ['400', '600', '700', '900'], @@ -22,18 +20,14 @@ export default function RootLayout({ }>) { return ( - + -
- -
{children}
-
-
+ {children}
diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..9e0fdbe --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,64 @@ +"use client"; +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; + +const LoginPage = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const router = useRouter(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + const response = await fetch("/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username, password }), + }); + + if (response.ok) { + router.push("/admin"); + } else { + const data = await response.json(); + setError(data.message); + } + }; + + return ( +
+

SVRJS ADMIN PANEL

+ {error &&

{error}

} +
+
+ setUsername(e.target.value)} + className="mt-1 block w-full bg-gray-800 rounded-full px-5 py-2 shadow-sm p-2" + /> +
+
+ setPassword(e.target.value)} + className="mt-1 block w-full bg-gray-800 rounded-full px-5 py-2 shadow-sm" + /> +
+ + +
+ ); +}; + +export default LoginPage; diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..4603f8b --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +