added vulnerabilties
This commit is contained in:
commit
94ff7b0ae0
159 changed files with 26455 additions and 0 deletions
12
.env.example
Normal file
12
.env.example
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
MONGODB_URI=
|
||||||
|
|
||||||
|
UPLOADTHING_SECRET=
|
||||||
|
UPLOADTHING_APP_ID=
|
||||||
|
|
||||||
|
ADMIN_USERNAME=
|
||||||
|
ADMIN_PASSWORD=
|
||||||
|
|
||||||
|
NEXTAUTH_SECRET=
|
||||||
|
|
||||||
|
EMAIL=
|
||||||
|
EMAIL_PASS=
|
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
36
README.md
Normal file
36
README.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
13
actions/login.action.ts
Normal file
13
actions/login.action.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// 'use server';
|
||||||
|
// import { NextApiRequest } from 'next';
|
||||||
|
// import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
// export async function POST(req: NextApiRequest) {
|
||||||
|
// const { username, password } = await req.body;
|
||||||
|
|
||||||
|
// if (username === process.env.USERNAME && password === process.env.PASSWORD) {
|
||||||
|
// return NextResponse.json({ success: true });
|
||||||
|
// } else {
|
||||||
|
// return NextResponse.json({ success: false });
|
||||||
|
// }
|
||||||
|
// }
|
23
app/(auth)/_components/Card.tsx
Normal file
23
app/(auth)/_components/Card.tsx
Normal file
|
@ -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<CardProps> = ({ title, url }) => {
|
||||||
|
return (
|
||||||
|
<div className=" bg-zinc-900 border rounded-lg hover:bg-zinc-800 transition-all">
|
||||||
|
<Link href={url} className="group">
|
||||||
|
<div className="flex-center rounded-lg p-6">
|
||||||
|
<h2 className="text-2xl font-bold mb-2">{title}</h2>
|
||||||
|
<ArrowUpRight className="w-5 h-5 mb-2 ml-2 opacity-0 group-hover:opacity-100 transition-all duration-300" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Card;
|
57
app/(auth)/_components/Mobilenav.tsx
Normal file
57
app/(auth)/_components/Mobilenav.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
"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";
|
||||||
|
import Logo from "@/components/shared/Logo";
|
||||||
|
|
||||||
|
const MobileNav = () => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
return (
|
||||||
|
<header className="header">
|
||||||
|
<Link href="/" className="flex items-center gap-2 md:py-2">
|
||||||
|
<Logo width={120} height={40} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<nav className="flex gap-2">
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className="sheet-content sm:w-64">
|
||||||
|
<>
|
||||||
|
<Logo width={155} height={53} />
|
||||||
|
<ul className="header-nav_elements">
|
||||||
|
{AdminLinks.slice(0, 6).map((link) => {
|
||||||
|
const isActive = link.url === pathname;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={link.url}
|
||||||
|
className={`${
|
||||||
|
isActive && "gradient-text"
|
||||||
|
} p-18 flex whitespace-nowrap text-dark-700`}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="sidebar-link cursor-pointer"
|
||||||
|
href={link.url}
|
||||||
|
>
|
||||||
|
<link.icon />
|
||||||
|
{link.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileNav;
|
65
app/(auth)/_components/Sidebar.tsx
Normal file
65
app/(auth)/_components/Sidebar.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { AdminLinks } from "@/constants";
|
||||||
|
import Logo from "@/components/shared/Logo";
|
||||||
|
|
||||||
|
const Sidebar = () => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<aside className="sidebar">
|
||||||
|
<div className="flex size-full flex-col gap-4">
|
||||||
|
<Link href="/" className="sidebar-logo">
|
||||||
|
<Logo width={155} height={53} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<nav className="sidebar-nav">
|
||||||
|
<ul className="sidebar-nav_elements">
|
||||||
|
{AdminLinks.slice(0, 5).map((link) => {
|
||||||
|
const isActive = link.url === pathname;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={link.url}
|
||||||
|
className={`sidebar-nav_element group ${
|
||||||
|
isActive ? "bg-white/5" : "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Link className="sidebar-link" href={link.url}>
|
||||||
|
<link.icon />
|
||||||
|
{link.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul className="sidebar-nav_elements">
|
||||||
|
{AdminLinks.slice(5).map((link) => {
|
||||||
|
const isActive = link.url === pathname;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={link.url}
|
||||||
|
className={`sidebar-nav_element group ${
|
||||||
|
isActive ? "bg-purple-gradient" : "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Link className="sidebar-link" href={link.url}>
|
||||||
|
<link.icon />
|
||||||
|
{link.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
9
app/(auth)/admin/changelogs/layout.tsx
Normal file
9
app/(auth)/admin/changelogs/layout.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Admin // Changelogs",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
232
app/(auth)/admin/changelogs/page.tsx
Normal file
232
app/(auth)/admin/changelogs/page.tsx
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useForm, SubmitHandler, useFieldArray } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { logsSchema } from "@/lib/validations/validation";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
_id: string;
|
||||||
|
version: string;
|
||||||
|
date: string;
|
||||||
|
bullets: { point: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogsFormValues = z.infer<typeof logsSchema>;
|
||||||
|
|
||||||
|
const AdminLogPage = () => {
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<LogsFormValues>({
|
||||||
|
resolver: zodResolver(logsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
version: "",
|
||||||
|
date: "",
|
||||||
|
bullets: [{ point: "" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "bullets",
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/logs", { method: "GET" });
|
||||||
|
if (response.ok) {
|
||||||
|
const data: LogEntry[] = await response.json();
|
||||||
|
setLogs(data);
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to fetch logs");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch("/api/uploadlogs", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
form.reset();
|
||||||
|
fetchLogs();
|
||||||
|
setLoading(false);
|
||||||
|
toast({ description: "Logs successfully added" });
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
toast({ description: "Upload Failed", variant: "destructive" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLog = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/delete/logs/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
fetchLogs();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to delete log");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogs();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchLogs();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="logs-page" className="wrapper container">
|
||||||
|
<h1 className="text-3xl font-bold py-6">Server Logs Form</h1>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="version"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Version Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="date"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Date</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Released on 24 Nov 2024" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<FormField
|
||||||
|
key={field.id}
|
||||||
|
control={form.control}
|
||||||
|
name={`bullets.${index}.point`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Key Point {index + 1}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mt-2"
|
||||||
|
variant={"secondary"}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mb-4"
|
||||||
|
size={"icon"}
|
||||||
|
variant={"outline"}
|
||||||
|
onClick={() => append({ point: "" })}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full text-lg rounded-full"
|
||||||
|
disabled={loading}
|
||||||
|
size={"lg"}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* Section to list and delete logs */}
|
||||||
|
<section id="logs-list" className="py-16 md:py-24">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold">Existing Logs</h2>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
<Table className="w-full mt-4 border-muted">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">Date</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{logs
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((log) => (
|
||||||
|
<TableRow key={log._id}>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{log.version}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{log.date}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
<Button
|
||||||
|
variant={"destructive"}
|
||||||
|
onClick={() => deleteLog(log._id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminLogPage;
|
9
app/(auth)/admin/downloads/layout.tsx
Normal file
9
app/(auth)/admin/downloads/layout.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Admin // Downloads",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
251
app/(auth)/admin/downloads/page.tsx
Normal file
251
app/(auth)/admin/downloads/page.tsx
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } 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 } from "@/lib/uploadthing";
|
||||||
|
import { downloadSchema } from "@/lib/validations/validation";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
|
interface DownloadEntry {
|
||||||
|
_id: string;
|
||||||
|
fileName: string;
|
||||||
|
version: string;
|
||||||
|
downloadLink: string;
|
||||||
|
fileSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DownloadsPage = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [downloads, setDownloads] = useState<DownloadEntry[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof downloadSchema>>({
|
||||||
|
resolver: zodResolver(downloadSchema),
|
||||||
|
defaultValues: {
|
||||||
|
fileName: "",
|
||||||
|
version: "",
|
||||||
|
downloadLink: "",
|
||||||
|
fileSize: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchDownloads = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/downloads", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data: DownloadEntry[] = await response.json();
|
||||||
|
setDownloads(data);
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to fetch downloads");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDownloads();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchDownloads();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<z.infer<typeof downloadSchema>> = async (
|
||||||
|
data
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch("/api/upload", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
form.reset();
|
||||||
|
fetchDownloads();
|
||||||
|
setLoading(false);
|
||||||
|
toast({ description: "Download Successfully Updated" });
|
||||||
|
} else {
|
||||||
|
console.error("Upload failed");
|
||||||
|
setLoading(false);
|
||||||
|
toast({ description: "Uploading Failed", variant: "destructive" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDownload = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/delete/downloads/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
fetchDownloads();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to delete download");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="downloads-page" className="wrapper container">
|
||||||
|
<h1 className="text-3xl font-bold py-6">Downloads Form</h1>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fileName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>File Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="version"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Version</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="downloadLink"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Download Link</FormLabel>
|
||||||
|
<UploadButton
|
||||||
|
endpoint="imageUploader"
|
||||||
|
onClientUploadComplete={(res) => {
|
||||||
|
field.onChange(res[0].url);
|
||||||
|
}}
|
||||||
|
onUploadError={(error: Error) => {
|
||||||
|
alert(`ERROR! ${error.message}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fileSize"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>File Size</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full text-lg rounded-full"
|
||||||
|
size={"lg"}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* Section to list and delete downloads */}
|
||||||
|
<section id="downloads-list" className="py-16 md:py-24">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold">Existing Downloads</h2>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
<Table className="w-full mt-4 border-muted">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">
|
||||||
|
Download Link
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{downloads
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((download) => (
|
||||||
|
<TableRow key={download._id}>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{download.fileName}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{download.version}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
<a
|
||||||
|
href={download.downloadLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{download.downloadLink}
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{download.fileSize}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
<Button
|
||||||
|
variant={"destructive"}
|
||||||
|
onClick={() => deleteDownload(download._id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadsPage;
|
9
app/(auth)/admin/mods/layout.tsx
Normal file
9
app/(auth)/admin/mods/layout.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Admin // Mods",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
254
app/(auth)/admin/mods/page.tsx
Normal file
254
app/(auth)/admin/mods/page.tsx
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } 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 } from "@/lib/uploadthing";
|
||||||
|
import { modsSchema } from "@/lib/validations/validation";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
|
interface ModEntry {
|
||||||
|
_id: string;
|
||||||
|
fileName: string;
|
||||||
|
version: string;
|
||||||
|
downloadLink: string;
|
||||||
|
fileSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SvrjsModsAdminPage = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [mods, setMods] = useState<ModEntry[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof modsSchema>>({
|
||||||
|
resolver: zodResolver(modsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
fileName: "",
|
||||||
|
version: "",
|
||||||
|
downloadLink: "",
|
||||||
|
fileSize: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchMods = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/mods", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data: ModEntry[] = await response.json();
|
||||||
|
setMods(data);
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to fetch mods");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMods();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchMods();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<z.infer<typeof modsSchema>> = async (data) => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch("/api/uploadmods", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
form.reset();
|
||||||
|
fetchMods();
|
||||||
|
setLoading(false);
|
||||||
|
toast({
|
||||||
|
description: "Successfully Uploaded Mods",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("Upload failed");
|
||||||
|
setLoading(false);
|
||||||
|
toast({
|
||||||
|
description: "Upload failed",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMod = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/delete/mods/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
fetchMods();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to delete mod");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="mods-page" className="wrapper container">
|
||||||
|
<h1 className="text-3xl font-bold py-6">Mods Form</h1>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fileName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>File Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="version"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Version</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="downloadLink"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Download Link</FormLabel>
|
||||||
|
<UploadButton
|
||||||
|
endpoint="imageUploader"
|
||||||
|
onClientUploadComplete={(res) => {
|
||||||
|
field.onChange(res[0].url);
|
||||||
|
}}
|
||||||
|
onUploadError={(error: Error) => {
|
||||||
|
alert(`ERROR! ${error.message}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fileSize"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>File Size</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full text-lg rounded-full"
|
||||||
|
size={"lg"}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* Section to list and delete mods */}
|
||||||
|
<section id="mods-list" className="py-16 md:py-24">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold">Existing Mods</h2>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
<Table className="w-full mt-4 border-muted">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">
|
||||||
|
Download Link
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{mods
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((mod) => (
|
||||||
|
<TableRow key={mod._id}>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{mod.fileName}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{mod.version}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
<a
|
||||||
|
href={mod.downloadLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{mod.downloadLink}
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{mod.fileSize}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
<Button
|
||||||
|
variant={"destructive"}
|
||||||
|
onClick={() => deleteMod(mod._id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SvrjsModsAdminPage;
|
79
app/(auth)/admin/multi-logs/[slug]/page.tsx
Normal file
79
app/(auth)/admin/multi-logs/[slug]/page.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const EditPage = ({ params }: { params: { slug: string } }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { slug } = params;
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (slug) {
|
||||||
|
fetch(`/api/mdx/pages/${slug}`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
setTitle(data.title);
|
||||||
|
setContent(data.content);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Failed to load page", error));
|
||||||
|
}
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
const savePage = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ title, content }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setLoading(false);
|
||||||
|
toast({ description: "Page successfully updated" });
|
||||||
|
router.push(`/admin/multi-logs/`);
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
// TEMPERARORY ERROR
|
||||||
|
router.push(`/admin/multi-logs/`);
|
||||||
|
toast({ description: "Updated but cant return data" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorChange = (value?: string) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setContent(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="edit-page" className="wrapper container">
|
||||||
|
<h1 className="text-3xl font-bold py-6">Edit Page: {slug}</h1>
|
||||||
|
<Input
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Page Title"
|
||||||
|
/>
|
||||||
|
<MarkdownEditor
|
||||||
|
value={content}
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
height={560}
|
||||||
|
/>
|
||||||
|
<Button onClick={savePage} disabled={loading} className="mt-4">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditPage;
|
9
app/(auth)/admin/multi-logs/layout.tsx
Normal file
9
app/(auth)/admin/multi-logs/layout.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Admin // MultiLogs",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
158
app/(auth)/admin/multi-logs/page.tsx
Normal file
158
app/(auth)/admin/multi-logs/page.tsx
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
interface PageEntry {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiLogs = () => {
|
||||||
|
const [pages, setPages] = useState<PageEntry[]>([]);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [pageTitle, setPageTitle] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/mdx/pages")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => setPages(data))
|
||||||
|
.catch((error) => console.error("Failed to load pages", error));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createPage = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const slug = pageTitle.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
const response = await fetch("/api/mdx/pages", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ title: pageTitle, slug, content: "" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const newPage = await response.json();
|
||||||
|
setPages([...pages, newPage]);
|
||||||
|
setPageTitle("");
|
||||||
|
setOpen(false);
|
||||||
|
router.push(`/admin/multi-logs/${slug}`);
|
||||||
|
toast({ description: "Page created successfully" });
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error("Failed to create page:", errorData);
|
||||||
|
toast({ description: `Error: ${errorData.message}` });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePage = async (slug: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setPages(pages.filter((page) => page.slug !== slug));
|
||||||
|
toast({ description: "Page deleted successfully" });
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error("Failed to delete page:", errorData);
|
||||||
|
toast({ description: `Error: ${errorData.message}` });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="logs-page" className="wrapper container">
|
||||||
|
<section id="create-page" className="py-16">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-2">Create New Page</h2>
|
||||||
|
<Button variant={"secondary"} onClick={() => setOpen(true)}>
|
||||||
|
Create New Page
|
||||||
|
</Button>
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Enter Page Title</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Input
|
||||||
|
value={pageTitle}
|
||||||
|
onChange={(e) => setPageTitle(e.target.value)}
|
||||||
|
placeholder="Page Title"
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button disabled={loading} onClick={createPage}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</section>
|
||||||
|
<section id="pages-list" className="pb-16">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold">Existing Pages</h2>
|
||||||
|
<p className="mb-4">Total Pages: {pages.length}</p>
|
||||||
|
<Table className="w-full mt-4 border-muted">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="border-b px-4 py-2">Slug</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{pages.map((page) => (
|
||||||
|
<TableRow key={page.slug}>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
<a
|
||||||
|
href={`/changelogs/${page.slug}`}
|
||||||
|
className="text-blue-500 underline"
|
||||||
|
>
|
||||||
|
{page.slug}
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/multi-logs/${page.slug}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={"destructive"}
|
||||||
|
onClick={() => deletePage(page.slug)}
|
||||||
|
className="ml-2"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultiLogs;
|
21
app/(auth)/admin/page.tsx
Normal file
21
app/(auth)/admin/page.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from "react";
|
||||||
|
import Card from "../_components/Card";
|
||||||
|
|
||||||
|
const AdminPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section id="adminpage" className="wrapper container">
|
||||||
|
<h1 className="h2-bold py-6">Admin Page</h1>
|
||||||
|
|
||||||
|
<div className="grid lg:grid-cols-2 grid-cols-1 gap-4 ">
|
||||||
|
<Card title="Downloads" url="/admin/downloads" />
|
||||||
|
<Card title="Mods" url="/admin/mods" />
|
||||||
|
<Card title="Logs" url="/admin/changelogs" />
|
||||||
|
<Card title="MultiLogs" url="/admin/multi-logs" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminPage;
|
9
app/(auth)/admin/vulnerabilities/layout.tsx
Normal file
9
app/(auth)/admin/vulnerabilities/layout.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Admin // Vulnerabilities",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
217
app/(auth)/admin/vulnerabilities/page.tsx
Normal file
217
app/(auth)/admin/vulnerabilities/page.tsx
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useForm, SubmitHandler, useFieldArray } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { logsSchema } from "@/lib/validations/validation";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
_id: string;
|
||||||
|
version: string;
|
||||||
|
date: string;
|
||||||
|
bullets: { point: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogsFormValues = z.infer<typeof logsSchema>;
|
||||||
|
|
||||||
|
const AdminLogPage = () => {
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<LogsFormValues>({
|
||||||
|
resolver: zodResolver(logsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
version: "",
|
||||||
|
date: "",
|
||||||
|
bullets: [{ point: "" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "bullets",
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/vulnerabilties", { method: "GET" });
|
||||||
|
if (response.ok) {
|
||||||
|
const data: LogEntry[] = await response.json();
|
||||||
|
setLogs(data);
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to fetch logs");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch("/api/uploadvulnerabilities", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
form.reset();
|
||||||
|
fetchLogs();
|
||||||
|
setLoading(false);
|
||||||
|
toast({ description: "Logs successfully added" });
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
toast({ description: "Upload Failed", variant: "destructive" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLog = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/delete/logs/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
fetchLogs();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to delete log");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogs();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchLogs();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="logs-page" className="wrapper container">
|
||||||
|
<h1 className="text-3xl font-bold py-6">Server Vulnerabilties Form</h1>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="version"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Version Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<FormField
|
||||||
|
key={field.id}
|
||||||
|
control={form.control}
|
||||||
|
name={`bullets.${index}.point`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Key Point {index + 1}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mt-2"
|
||||||
|
variant={"secondary"}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mb-4"
|
||||||
|
size={"icon"}
|
||||||
|
variant={"outline"}
|
||||||
|
onClick={() => append({ point: "" })}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full text-lg rounded-full"
|
||||||
|
disabled={loading}
|
||||||
|
size={"lg"}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* Section to list and delete logs */}
|
||||||
|
<section id="logs-list" className="py-16 md:py-24">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold">
|
||||||
|
Existing Vulnerabilties
|
||||||
|
</h2>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
<Table className="w-full mt-4 border-muted">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||||
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{logs
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((log) => (
|
||||||
|
<TableRow key={log._id}>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
{log.version}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="border-b px-4 py-2">
|
||||||
|
<Button
|
||||||
|
variant={"destructive"}
|
||||||
|
onClick={() => deleteLog(log._id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminLogPage;
|
16
app/(auth)/layout.tsx
Normal file
16
app/(auth)/layout.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import MobileNav from "./_components/Mobilenav";
|
||||||
|
import Sidebar from "./_components/Sidebar";
|
||||||
|
|
||||||
|
export default function PageLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col min-h-screen root">
|
||||||
|
<Sidebar />
|
||||||
|
<MobileNav />
|
||||||
|
<div className="root-container lg:px-24">{children}</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
11
app/(root)/blog/page.tsx
Normal file
11
app/(root)/blog/page.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Blog - SVRJS",
|
||||||
|
};
|
||||||
|
const BlogPage = () => {
|
||||||
|
return <div>BlogPage</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlogPage;
|
87
app/(root)/changelogs/[slug]/page.tsx
Normal file
87
app/(root)/changelogs/[slug]/page.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
"use client";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
const Page = ({ params }: { params: { slug: string } }) => {
|
||||||
|
const { slug } = params;
|
||||||
|
const [page, setPage] = useState<{ title: string; content: string } | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPage = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mdx/pages/${slug}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setPage(data);
|
||||||
|
return (document.title = `${data.title} | SVRJS`);
|
||||||
|
} else {
|
||||||
|
if (response.status === 404) {
|
||||||
|
setNotFound(true);
|
||||||
|
return (document.title = "404 Not Found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load page", error);
|
||||||
|
setNotFound(true);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPage();
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||||
|
<div className="mb-3">
|
||||||
|
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
||||||
|
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||||
|
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||||
|
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notFound) {
|
||||||
|
return (
|
||||||
|
<section id="404error" className="flex-center flex-col wrapper container">
|
||||||
|
<h1 className="text-3xl md:text-5xl text-center">
|
||||||
|
<span className="text-red-500">404</span> Page not Found
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg mt-3 text-muted-foreground">
|
||||||
|
Please return back to Home
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="wrapper container py-24 md:py-28 gap-2 flex flex-col">
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
{page.title}
|
||||||
|
</h1>
|
||||||
|
<ReactMarkdown className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
|
{page.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
86
app/(root)/changelogs/page.tsx
Normal file
86
app/(root)/changelogs/page.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { CHANGE_LOGS } from "@/constants/guidelines";
|
||||||
|
|
||||||
|
interface Bullet {
|
||||||
|
point: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LOGS {
|
||||||
|
_id: string;
|
||||||
|
date: string;
|
||||||
|
version: string;
|
||||||
|
bullets?: Bullet[]; // Make bullets optional
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogsPage: React.FC = () => {
|
||||||
|
const [downloads, setDownloads] = useState<LOGS[]>([]);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const fetchDownloads = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/logs", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data: LOGS[] = await response.json();
|
||||||
|
setDownloads(data);
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to fetch downloads");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDownloads();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchDownloads();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
const reversedDownloads = [...downloads].reverse();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="logs"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
Server LOGS
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
Get all the latest version of SVRJS download and compiled Files here!
|
||||||
|
</p>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
|
||||||
|
{reversedDownloads.map((download) => (
|
||||||
|
<div
|
||||||
|
key={download._id}
|
||||||
|
className="flex-start prose max-w-full md:prose-lg dark:prose-invert flex-col mb-4"
|
||||||
|
>
|
||||||
|
<h2 className="font-bold text-3xl">{download.version}</h2>
|
||||||
|
<span className="font-medium italic">{download.date}</span>
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
{(download.bullets ?? []).map((bullet, index) => (
|
||||||
|
<li key={index}>{bullet.point}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
|
<ReactMarkdown>{CHANGE_LOGS}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsPage;
|
179
app/(root)/contact/page.tsx
Normal file
179
app/(root)/contact/page.tsx
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
"use client";
|
||||||
|
import Iconss from "@/components/ui/icons";
|
||||||
|
import { Mail, Send, WebhookIcon, Bug, Shield } from "lucide-react";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { contactFormSchema } from "@/lib/validations/validation";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { emails } from "@/constants";
|
||||||
|
|
||||||
|
const ContactUs = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof contactFormSchema>>({
|
||||||
|
resolver: zodResolver(contactFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
message: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/contact", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
form.reset();
|
||||||
|
toast({
|
||||||
|
description: "Your message has been sent.",
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Uh oh! Something went wrong.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast({
|
||||||
|
title: "Uh oh! Something went wrong.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300">
|
||||||
|
<h1 className="text-4xl md:text-6xl tracking-tight font-bold uppercase text-center text-gray-900 dark:text-white">
|
||||||
|
Contact Us
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="contact" className="w-full">
|
||||||
|
<div className="flex-center flex-col md:flex-row justify-between mx-auto p-6 max-w-5xl">
|
||||||
|
{/* Left contact page */}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-4 pb-8 mb-8 max-w-lg w-full bg-accent border p-6 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Your Name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="email" placeholder="Your Email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="message"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Message</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className="h-44"
|
||||||
|
placeholder="Your Message"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant={"default"}
|
||||||
|
className="w-full mt-2"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<span className="tracking-tight font-semibold">SEND</span>
|
||||||
|
<Send className="ml-2 w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* Right contact page */}
|
||||||
|
<div className="max-w-lg mt-8 md:mt-0 md:ml-8 p-12 border rounded-lg">
|
||||||
|
<ul className="space-y-4 mb-6">
|
||||||
|
{emails.map((email, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="text-gray-600 dark:text-gray-300 flex items-center"
|
||||||
|
>
|
||||||
|
<email.icon className="mr-2" size={24} />
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
href={email.url}
|
||||||
|
title={`Send an email to ${email.email}`}
|
||||||
|
className="text-muted-foreground hover:text-accent-foreground transition duration-200"
|
||||||
|
>
|
||||||
|
{email.email}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Separator />
|
||||||
|
<ul className="flex justify-center space-x-3 my-6">
|
||||||
|
<Iconss />
|
||||||
|
</ul>
|
||||||
|
<Separator />
|
||||||
|
<div className="text-center text-gray-500 mt-2 text-sm font-light"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactUs;
|
29
app/(root)/contribute/page.tsx
Normal file
29
app/(root)/contribute/page.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { contribute } from "@/constants/guidelines";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Contribute - SVRJS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const Contribute = () => {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="contribute"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
Contributing to SVR.JS
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
We welcome contributions from the community! Here's how you can
|
||||||
|
help!
|
||||||
|
</p>
|
||||||
|
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
|
<ReactMarkdown>{contribute}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contribute;
|
13
app/(root)/downloads/layout.tsx
Normal file
13
app/(root)/downloads/layout.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Downloads - SVRJS",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DownloadLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <main>{children}</main>;
|
||||||
|
}
|
106
app/(root)/downloads/page.tsx
Normal file
106
app/(root)/downloads/page.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
interface Download {
|
||||||
|
_id: string;
|
||||||
|
date: string;
|
||||||
|
fileName: string;
|
||||||
|
version: string;
|
||||||
|
fileSize: string;
|
||||||
|
downloadLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DownloadPage: React.FC = () => {
|
||||||
|
const [downloads, setDownloads] = useState<Download[]>([]);
|
||||||
|
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);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="download"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
Downloads
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
Get all the latest version of SVRJS download and compiled Files here!
|
||||||
|
</p>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
<Table>
|
||||||
|
<TableCaption>A list of all available downloads.</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[150px]">Date</TableHead>
|
||||||
|
<TableHead>File Name</TableHead>
|
||||||
|
<TableHead>Version</TableHead>
|
||||||
|
<TableHead>File Size</TableHead>
|
||||||
|
<TableHead className="text-right">Download Link</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{downloads
|
||||||
|
.slice(0, 10)
|
||||||
|
.reverse()
|
||||||
|
.map((download) => (
|
||||||
|
<TableRow key={download._id}>
|
||||||
|
<TableCell className="font-medium">{download.date}</TableCell>
|
||||||
|
<TableCell>{download.fileName}</TableCell>
|
||||||
|
<TableCell>{download.version}</TableCell>
|
||||||
|
<TableCell className="text-left">{download.fileSize}</TableCell>
|
||||||
|
<TableCell className="flex items-center justify-end">
|
||||||
|
<Link href={download.downloadLink}>
|
||||||
|
<Button variant={"ghost"} className="">
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadPage;
|
13
app/(root)/forum/page.tsx
Normal file
13
app/(root)/forum/page.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Forum - SVRJS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const Forum = () => {
|
||||||
|
return <div>Forum</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Forum;
|
47
app/(root)/layout.tsx
Normal file
47
app/(root)/layout.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import Footer from "@/components/shared/Footer";
|
||||||
|
import Navbar from "@/components/shared/Navbar";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "SVRJS - A Web Server running on Node.js",
|
||||||
|
description:
|
||||||
|
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||||
|
openGraph: {
|
||||||
|
title: "SVRJS - A Web Server running on Node.js",
|
||||||
|
description:
|
||||||
|
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||||
|
url: "https://svrjs.org",
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "SVRJS - A Web Server running on Node.js",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "SVRJS - A Web Server running on Node.js",
|
||||||
|
description:
|
||||||
|
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||||
|
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||||
|
creator: "@SVR_JS",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default function PageLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<div className="flex-grow flex-1 overflow-x-hidden">{children}</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
11
app/(root)/mods/layout.tsx
Normal file
11
app/(root)/mods/layout.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "MOD - SVRJS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <main>{children}</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModLayout;
|
106
app/(root)/mods/page.tsx
Normal file
106
app/(root)/mods/page.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
interface Mods {
|
||||||
|
_id: string;
|
||||||
|
date: string;
|
||||||
|
fileName: string;
|
||||||
|
version: string;
|
||||||
|
fileSize: string;
|
||||||
|
downloadLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModsPage: React.FC = () => {
|
||||||
|
const [downloads, setDownloads] = useState<Mods[]>([]);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const fetchDownloads = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/mods", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data: Mods[] = 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);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="mods"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
SvrJS Mods
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
Get all the latest version of SVRJS Mods and compiled Files here!{" "}
|
||||||
|
</p>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
<Table>
|
||||||
|
<TableCaption>A list of all available downloads.</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[150px]">Date</TableHead>
|
||||||
|
<TableHead>File Name</TableHead>
|
||||||
|
<TableHead>Version</TableHead>
|
||||||
|
<TableHead>Download Link</TableHead>
|
||||||
|
<TableHead className="text-right">File Size</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{downloads
|
||||||
|
.slice(0, 10)
|
||||||
|
.reverse()
|
||||||
|
.map((download) => (
|
||||||
|
<TableRow key={download._id}>
|
||||||
|
<TableCell className="font-medium">{download.date}</TableCell>
|
||||||
|
<TableCell>{download.fileName}</TableCell>
|
||||||
|
<TableCell>{download.version}</TableCell>
|
||||||
|
<TableCell className="text-left">{download.fileSize}</TableCell>
|
||||||
|
<TableCell className="flex items-center justify-end">
|
||||||
|
<Link href={download.downloadLink}>
|
||||||
|
<Button variant={"ghost"} className="">
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModsPage;
|
25
app/(root)/page.tsx
Normal file
25
app/(root)/page.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import About from "@/components/shared/About";
|
||||||
|
import DataTable from "@/components/shared/DataTable";
|
||||||
|
import Faq from "@/components/shared/FAQ";
|
||||||
|
import Hero from "@/components/shared/Hero";
|
||||||
|
import HowItWorks from "@/components/shared/HowItWorks";
|
||||||
|
import Newsletter from "@/components/shared/Newsletter";
|
||||||
|
import Partners from "@/components/shared/Partners";
|
||||||
|
import Testimonials from "@/components/shared/Testimonials";
|
||||||
|
|
||||||
|
const RootPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero />
|
||||||
|
<HowItWorks />
|
||||||
|
<Testimonials />
|
||||||
|
<Partners />
|
||||||
|
<About />
|
||||||
|
{/* <DataTable /> */}
|
||||||
|
<Faq />
|
||||||
|
<Newsletter />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RootPage;
|
30
app/(root)/privacy-policy/page.tsx
Normal file
30
app/(root)/privacy-policy/page.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { PRIVACY_POLICY } from "@/constants/guidelines";
|
||||||
|
import React from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Privacy Policy - SVRJS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PrivacyPolicy = () => {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="privacy-policy"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
Privacy Policy
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
Effective date: 26.05.2024
|
||||||
|
</p>
|
||||||
|
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
|
<ReactMarkdown>{PRIVACY_POLICY}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivacyPolicy;
|
28
app/(root)/tos/page.tsx
Normal file
28
app/(root)/tos/page.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { TERMS_AND_CONDITIONS } from "@/constants/guidelines";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Terms Of Service - SVRJS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const TermsOfService = () => {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="tos"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
Terms and Conditions
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
Last updated: 24.04.2024
|
||||||
|
</p>
|
||||||
|
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
|
<ReactMarkdown>{TERMS_AND_CONDITIONS}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TermsOfService;
|
108
app/(root)/vulnerabilities/page.tsx
Normal file
108
app/(root)/vulnerabilities/page.tsx
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { vulnerabilities } from "@/constants/guidelines";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
interface Bullet {
|
||||||
|
point: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Vulnerabilities {
|
||||||
|
_id: string;
|
||||||
|
date: string;
|
||||||
|
version: string;
|
||||||
|
bullets?: Bullet[]; // Make bullets optional
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vulnerabilities = () => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [downloads, setDownloads] = useState<Vulnerabilities[]>([]);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/vulnerabilities", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data: Vulnerabilities[] = await response.json();
|
||||||
|
setDownloads(data);
|
||||||
|
return (document.title = "Vulnerabilities | SVRJS");
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to fetch downloads");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchData();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
const reversedDownloads = [...downloads].reverse();
|
||||||
|
|
||||||
|
// initially loading = true
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||||
|
<div className="mb-3">
|
||||||
|
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
||||||
|
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||||
|
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||||
|
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="vulnerabilities"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
SVR.JS Vulnerabilities
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
Some older versions of SVR.JS are vulnerable to cyberattacks. It's
|
||||||
|
recommended to update your SVR.JS version to the newest one. If you find
|
||||||
|
a security issue with SVR.JS, report it as soon as possible to
|
||||||
|
vulnerability-reports[at]svrjs[dot]org. We'll mitigate that
|
||||||
|
vulnerability if it is possible.
|
||||||
|
</p>
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
|
||||||
|
{reversedDownloads.map((download) => (
|
||||||
|
<div
|
||||||
|
key={download._id}
|
||||||
|
className="flex-start flex-col prose dark:prose-invert mb-4 gap-4"
|
||||||
|
>
|
||||||
|
<h2 className="font-semibold text-3xl -mb-2">{download.version}</h2>
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
{(download.bullets ?? []).map((bullet, index) => (
|
||||||
|
<li key={index}>{bullet.point}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
|
<ReactMarkdown>{vulnerabilities}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Vulnerabilities;
|
63
app/api/auth/[...nextauth]/options.ts
Normal file
63
app/api/auth/[...nextauth]/options.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { NextAuthOptions } from "next-auth";
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
providers: [
|
||||||
|
CredentialsProvider({
|
||||||
|
id: "credentials",
|
||||||
|
name: "Credentials",
|
||||||
|
credentials: {
|
||||||
|
username: { label: "Username", type: "text" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
async authorize(credentials: any): Promise<any> {
|
||||||
|
const adminUsername = process.env.ADMIN_USERNAME;
|
||||||
|
const adminPasswordHash = process.env.ADMIN_PASSWORD;
|
||||||
|
|
||||||
|
console.log(adminUsername);
|
||||||
|
console.log(adminPasswordHash);
|
||||||
|
console.log(credentials.username);
|
||||||
|
console.log(credentials.password);
|
||||||
|
if (credentials.username == adminUsername) {
|
||||||
|
const isValidPassword = await bcrypt.compare(
|
||||||
|
credentials.password,
|
||||||
|
adminPasswordHash!
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(isValidPassword);
|
||||||
|
|
||||||
|
if (isValidPassword) {
|
||||||
|
// aany object returned will be saved in `user` property of the jwtt
|
||||||
|
return { id: 1, name: "svrjsAdmin" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If you return null then an error will be displayed that the user to check their details.
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, user }) {
|
||||||
|
// Add user info to token
|
||||||
|
if (user) {
|
||||||
|
token.id = user.id;
|
||||||
|
token.name = user.name;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
// Add token info to session
|
||||||
|
// session.user.id = token.id;
|
||||||
|
// session.user.name = token.name;
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: "/login",
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
|
};
|
6
app/api/auth/[...nextauth]/route.ts
Normal file
6
app/api/auth/[...nextauth]/route.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import NextAuth from "next-auth/next";
|
||||||
|
import { authOptions } from "./options";
|
||||||
|
|
||||||
|
const handler = NextAuth(authOptions);
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST };
|
126
app/api/contact/route.ts
Normal file
126
app/api/contact/route.ts
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { mailOptions, transporter } from "@/lib/nodemailer/nodemailer";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const CONTACT_MESSAGE_FIELDS: Record<string, string> = {
|
||||||
|
name: "Name",
|
||||||
|
email: "Email",
|
||||||
|
message: "Message",
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeHtml = (text: string) => {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateEmailContent = (data: Record<string, string>) => {
|
||||||
|
const stringData = Object.entries(data).reduce(
|
||||||
|
(str, [key, val]) =>
|
||||||
|
str +
|
||||||
|
`${CONTACT_MESSAGE_FIELDS[key] || key}: ${val.replace(/\n/g, "\n")} \n\n`,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
const htmlData = Object.entries(data).reduce(
|
||||||
|
(str, [key, val]) =>
|
||||||
|
str +
|
||||||
|
`<h3 class="form-heading">${escapeHtml(
|
||||||
|
CONTACT_MESSAGE_FIELDS[key] || key
|
||||||
|
)}</h3><p class="form-answer">${escapeHtml(val).replace(
|
||||||
|
/\n/g,
|
||||||
|
"<br/>"
|
||||||
|
)}</p>`,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: stringData,
|
||||||
|
html: `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Contact Email</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||||
|
<style type="text/css">
|
||||||
|
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||||
|
table { border-collapse: collapse !important; }
|
||||||
|
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||||
|
@media screen and (max-width: 525px) {
|
||||||
|
.wrapper { width: 100% !important; max-width: 100% !important; }
|
||||||
|
.responsive-table { width: 100% !important; }
|
||||||
|
.padding { padding: 10px 5% 15px 5% !important; }
|
||||||
|
.section-padding { padding: 0 15px 50px 15px !important; }
|
||||||
|
}
|
||||||
|
.form-container { margin-bottom: 24px; padding: 20px; border: 1px dashed #ccc; }
|
||||||
|
.form-heading { color: #2a2a2a; font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif; font-weight: 400; text-align: left; line-height: 20px; font-size: 18px; margin: 0 0 8px; padding: 0; }
|
||||||
|
.form-answer { color: #2a2a2a; font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif; font-weight: 300; text-align: left; line-height: 20px; font-size: 16px; margin: 0 0 24px; padding: 0; }
|
||||||
|
div[style*="margin: 16px 0;"] { margin: 0 !important; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0 !important; padding: 0 !important; background: #fff">
|
||||||
|
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;"></div>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" style="padding: 10px 15px 30px 15px" class="section-padding">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 500px" class="responsive-table">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 0 0 0 0; font-size: 16px; line-height: 25px; color: #232323;" class="padding message-content">
|
||||||
|
<h2>New Contact Message</h2>
|
||||||
|
<div class="form-container">${htmlData}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Method Not Allowed" },
|
||||||
|
{ status: 405 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await req.json();
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
...mailOptions,
|
||||||
|
...generateEmailContent(data),
|
||||||
|
subject: "Contact Email",
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Email sent successfully" },
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending email:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
app/api/delete/downloads/[id]/route.ts
Normal file
30
app/api/delete/downloads/[id]/route.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// app/api/delete/[id]/route.ts
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import { ObjectId } from "mongodb";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db("downloadsDatabase");
|
||||||
|
const collection = db.collection("downloads");
|
||||||
|
|
||||||
|
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||||
|
|
||||||
|
if (result.deletedCount === 1) {
|
||||||
|
return NextResponse.json({ message: "Log deleted successfully" });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Failed to delete log", error: error },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
app/api/delete/logs/[id]/route.ts
Normal file
30
app/api/delete/logs/[id]/route.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// app/api/delete/[id]/route.ts
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import { ObjectId } from "mongodb";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db("downloadsDatabase");
|
||||||
|
const collection = db.collection("logs");
|
||||||
|
|
||||||
|
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||||
|
|
||||||
|
if (result.deletedCount === 1) {
|
||||||
|
return NextResponse.json({ message: "Log deleted successfully" });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Failed to delete log", error: error },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
app/api/delete/mods/[id]/route.ts
Normal file
30
app/api/delete/mods/[id]/route.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// app/api/delete/[id]/route.ts
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import { ObjectId } from "mongodb";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db("downloadsDatabase");
|
||||||
|
const collection = db.collection("mods");
|
||||||
|
|
||||||
|
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||||
|
|
||||||
|
if (result.deletedCount === 1) {
|
||||||
|
return NextResponse.json({ message: "Log deleted successfully" });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Failed to delete log", error: error },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
22
app/api/downloads/route.ts
Normal file
22
app/api/downloads/route.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
// console.log("Downloads fetched:", downloads);
|
||||||
|
return NextResponse.json(downloads, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error Messge ${error}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch downloads" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
34
app/api/login/route.ts
Normal file
34
app/api/login/route.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { serialize } from "cookie";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const { username, password } = await request.json();
|
||||||
|
|
||||||
|
const adminUsername = process.env.ADMIN_USERNAME;
|
||||||
|
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||||
|
|
||||||
|
if (username === adminUsername && password === adminPassword) {
|
||||||
|
const cookie = serialize("auth", "authenticated", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
maxAge: 60 * 60 * 24, // 1 day
|
||||||
|
});
|
||||||
|
|
||||||
|
return new NextResponse(JSON.stringify({ message: "Login successful" }), {
|
||||||
|
headers: {
|
||||||
|
"Set-Cookie": cookie,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), {
|
||||||
|
status: 401,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
20
app/api/logs/route.ts
Normal file
20
app/api/logs/route.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
// 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("logs").find().toArray();
|
||||||
|
return NextResponse.json(downloads, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch logs" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
127
app/api/mdx/pages/[slug]/route.ts
Normal file
127
app/api/mdx/pages/[slug]/route.ts
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { slug: string } }
|
||||||
|
) => {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db();
|
||||||
|
const { slug } = params;
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await db.collection("pages").findOne({ slug });
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
return NextResponse.json(page, { status: 200 });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PUT = async (
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { slug: string } }
|
||||||
|
) => {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db();
|
||||||
|
const { slug } = params;
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, content } = await req.json();
|
||||||
|
|
||||||
|
if (typeof title !== "string" || typeof content !== "string") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Invalid title or content" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// it works here ig
|
||||||
|
const result = await db
|
||||||
|
.collection("pages")
|
||||||
|
.findOneAndUpdate(
|
||||||
|
{ slug },
|
||||||
|
{ $set: { title, content } },
|
||||||
|
{ returnDocument: "after" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// i hate my life fr fr
|
||||||
|
console.log("Update Result:", result);
|
||||||
|
// result returns like
|
||||||
|
|
||||||
|
// Update Result: {
|
||||||
|
// _id: new ObjectId('66a2946b2b91eef505eef943'),
|
||||||
|
// title: 'TEST PAGE',
|
||||||
|
// slug: 'test-page',
|
||||||
|
// content: 'asd]---\n' +
|
||||||
|
// '---\n' +
|
||||||
|
// '\n' +
|
||||||
|
// 'this is basic heading ?\n' +
|
||||||
|
// '\n' +
|
||||||
|
// '**HELLO**\n' +
|
||||||
|
// '\n' +
|
||||||
|
// 'erw\n' +
|
||||||
|
// '\n' +
|
||||||
|
// 'trying another time for test'
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ERRROR : TypeError: Cannot read properties of undefined (reading '_id')
|
||||||
|
// aposdjaoi sdio JUST WORK NIAWWWWWWWWW
|
||||||
|
|
||||||
|
// if (result && result.value) {
|
||||||
|
const serializedResult = {
|
||||||
|
...result?.value,
|
||||||
|
_id: result?.value._id.toString(), // Convert ObjectId to string
|
||||||
|
};
|
||||||
|
return NextResponse.json(result?.value.content, { status: 200 });
|
||||||
|
// } else {
|
||||||
|
// return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating page:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Failed to update page" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE = async (
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { slug: string } }
|
||||||
|
) => {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db();
|
||||||
|
const { slug } = params;
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.collection("pages").deleteOne({ slug });
|
||||||
|
|
||||||
|
if (result.deletedCount > 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Page deleted successfully" },
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting page:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Failed to delete page" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
43
app/api/mdx/pages/route.ts
Normal file
43
app/api/mdx/pages/route.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
export const GET = async (req: NextRequest) => {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pages = await db.collection("pages").find().toArray();
|
||||||
|
return NextResponse.json(pages, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching pages:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Failed to fetch pages" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = async (req: NextRequest) => {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db();
|
||||||
|
const { title, slug, content } = await req.json();
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Missing required fields" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPage = { title, slug, content };
|
||||||
|
const result = await db.collection("pages").insertOne(newPage);
|
||||||
|
return NextResponse.json(newPage, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating page:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Failed to create page" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
20
app/api/mods/route.ts
Normal file
20
app/api/mods/route.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
// 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("mods").find().toArray();
|
||||||
|
return NextResponse.json(downloads, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch mods" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
23
app/api/upload/route.ts
Normal file
23
app/api/upload/route.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
21
app/api/uploadlogs/route.ts
Normal file
21
app/api/uploadlogs/route.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json();
|
||||||
|
const { version, date, bullets } = body;
|
||||||
|
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db("downloadsDatabase");
|
||||||
|
|
||||||
|
const result = await db.collection("logs").insertOne({
|
||||||
|
version,
|
||||||
|
date,
|
||||||
|
bullets,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, id: result.insertedId });
|
||||||
|
}
|
23
app/api/uploadmods/route.ts
Normal file
23
app/api/uploadmods/route.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
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("mods").insertOne({
|
||||||
|
date: new Date().toISOString().split("T")[0],
|
||||||
|
fileName,
|
||||||
|
version,
|
||||||
|
downloadLink,
|
||||||
|
fileSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, id: result.insertedId });
|
||||||
|
}
|
22
app/api/uploadthing/core.ts
Normal file
22
app/api/uploadthing/core.ts
Normal file
|
@ -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;
|
9
app/api/uploadthing/route.ts
Normal file
9
app/api/uploadthing/route.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { createRouteHandler } from "uploadthing/next";
|
||||||
|
import { ourFileRouter } from "./core";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export const { GET, POST } = createRouteHandler({
|
||||||
|
router: ourFileRouter,
|
||||||
|
});
|
20
app/api/uploadvulnerabilities/route.ts
Normal file
20
app/api/uploadvulnerabilities/route.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json();
|
||||||
|
const { version, date, bullets } = body;
|
||||||
|
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db("downloadsDatabase");
|
||||||
|
|
||||||
|
const result = await db.collection("vulnerabilities").insertOne({
|
||||||
|
version,
|
||||||
|
bullets,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, id: result.insertedId });
|
||||||
|
}
|
20
app/api/vulnerabilities/route.ts
Normal file
20
app/api/vulnerabilities/route.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
// Force the API to use SSR instead of static generation
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
// 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("vulnerabilities").find().toArray();
|
||||||
|
return NextResponse.json(downloads, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch logs" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
261
app/globals.css
Normal file
261
app/globals.css
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--svg-fill: white;
|
||||||
|
--svg-background: black;
|
||||||
|
--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: 142.1 76.2% 36.3%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--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: 142.1 76.2% 36.3%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--svg-fill: black;
|
||||||
|
--svg-background: white;
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--foreground: 0 0% 95%;
|
||||||
|
--card: 24 9.8% 10%;
|
||||||
|
--card-foreground: 0 0% 95%;
|
||||||
|
--popover: 0 0% 9%;
|
||||||
|
--popover-foreground: 0 0% 95%;
|
||||||
|
--primary: 142.1 70.6% 45.3%;
|
||||||
|
--primary-foreground: 144.9 80.4% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 15%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 142.4 71.8% 29.2%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground w-full h-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.wrapper {
|
||||||
|
@apply max-w-screen-xl lg:mx-auto p-5 md:px-10 xl:px-0 w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
@apply flex justify-center items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
@apply flex justify-between items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-start {
|
||||||
|
@apply flex justify-between items-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 */
|
||||||
|
.h1-bold {
|
||||||
|
@apply font-bold text-[40px] leading-[48px] lg:text-[48px] lg:leading-[60px] xl:text-[58px] xl:leading-[74px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 40 */
|
||||||
|
.h2-bold {
|
||||||
|
@apply font-bold text-[32px] leading-[40px] lg:text-[36px] lg:leading-[44px] xl:text-[40px] xl:leading-[48px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.h2-medium {
|
||||||
|
@apply font-medium text-[32px] leading-[40px] lg:text-[36px] lg:leading-[44px] xl:text-[40px] xl:leading-[48px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 36 */
|
||||||
|
.h3-bold {
|
||||||
|
@apply font-bold text-[28px] leading-[36px] md:text-[36px] md:leading-[44px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.h3-medium {
|
||||||
|
@apply font-medium text-[28px] leading-[36px] md:text-[36px] md:leading-[44px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 32 */
|
||||||
|
.h4-medium {
|
||||||
|
@apply font-medium text-[32px] leading-[40px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 28 */
|
||||||
|
.h5-bold {
|
||||||
|
@apply font-bold text-[28px] leading-[36px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 24 */
|
||||||
|
.p-bold-24 {
|
||||||
|
@apply font-bold text-[24px] leading-[36px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-medium-24 {
|
||||||
|
@apply font-medium text-[24px] leading-[36px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-regular-24 {
|
||||||
|
@apply font-normal text-[24px] leading-[36px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 20 */
|
||||||
|
.p-bold-20 {
|
||||||
|
@apply font-bold text-[20px] leading-[30px] tracking-[2%];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-semibold-20 {
|
||||||
|
@apply text-[20px] font-semibold leading-[30px] tracking-[2%];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-medium-20 {
|
||||||
|
@apply text-[20px] font-medium leading-[30px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-regular-20 {
|
||||||
|
@apply text-[20px] font-normal leading-[30px] tracking-[2%];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 18 */
|
||||||
|
.p-semibold-18 {
|
||||||
|
@apply text-[18px] font-semibold leading-[28px] tracking-[2%];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-medium-18 {
|
||||||
|
@apply text-[18px] font-medium leading-[28px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-regular-18 {
|
||||||
|
@apply text-[18px] font-normal leading-[28px] tracking-[2%];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 16 */
|
||||||
|
.p-bold-16 {
|
||||||
|
@apply text-[16px] font-bold leading-[24px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-medium-16 {
|
||||||
|
@apply text-[16px] font-medium leading-[24px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-regular-16 {
|
||||||
|
@apply text-[16px] font-normal leading-[24px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 14 */
|
||||||
|
.p-semibold-14 {
|
||||||
|
@apply text-[14px] font-semibold leading-[20px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-medium-14 {
|
||||||
|
@apply text-[14px] font-medium leading-[20px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-regular-14 {
|
||||||
|
@apply text-[14px] font-normal leading-[20px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 12 */
|
||||||
|
.p-medium-12 {
|
||||||
|
@apply text-[12px] font-medium leading-[20px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
@apply bg-gray-300 !important;
|
||||||
|
}
|
||||||
|
}
|
40
app/layout.tsx
Normal file
40
app/layout.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Poppins } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import { ThemeProvider } from "@/components/shared/providers/themeprovider";
|
||||||
|
import AuthProvider from "@/components/shared/providers/AuthProvider";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
||||||
|
const poppins = Poppins({
|
||||||
|
weight: ["400", "600", "700", "900"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "SVRJS - A Web Server running on Nodejs",
|
||||||
|
description: "Open Source Software Library",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body className={`antialiased ${poppins.className}`}>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="dark"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<AuthProvider>
|
||||||
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
76
app/login/page.tsx
Normal file
76
app/login/page.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const router = useRouter();
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
|
if (status === "loading") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="h-screen w-full flex-center">
|
||||||
|
<p className="animate-pulse text-xl">Loading</p>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
router.push("/admin");
|
||||||
|
}
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const res = await signIn("credentials", {
|
||||||
|
redirect: false,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.ok) {
|
||||||
|
router.push("/admin");
|
||||||
|
} else {
|
||||||
|
setError("Invalid credentials");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-xl h-screen flex justify-center bg-gray-900 flex-col container">
|
||||||
|
<h1 className="text-3xl font-bold mb-4">SVRJS ADMIN PANEL</h1>
|
||||||
|
{error && <p className="text-red-500 mb-4">{error}</p>}
|
||||||
|
<form onSubmit={handleLogin}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="mt-1 block w-full bg-gray-800 rounded-full px-5 py-2 shadow-sm p-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
id="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="mt-1 block w-full bg-gray-800 rounded-full px-5 py-2 shadow-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" className="w-full rounded-full" size={"lg"}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
19
app/not-found.tsx
Normal file
19
app/not-found.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
return (
|
||||||
|
<section id="404error" className="flex-center flex-col wrapper container">
|
||||||
|
<h1 className="text-3xl md:text-5xl text-center">
|
||||||
|
<span className="text-red-500">404</span> Page not Found
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg mt-3 text-muted-foreground">
|
||||||
|
Please return back to{" "}
|
||||||
|
<Link href="/" className="underline font-bold">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
20
app/sitemap.ts
Normal file
20
app/sitemap.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
export default async function sitemap() {
|
||||||
|
let routes = [
|
||||||
|
"",
|
||||||
|
"/blog",
|
||||||
|
"/changelogs",
|
||||||
|
"/contact",
|
||||||
|
"/contribute",
|
||||||
|
"/downloads",
|
||||||
|
"/forum",
|
||||||
|
"/mods",
|
||||||
|
"/privacy-policy",
|
||||||
|
"/tos",
|
||||||
|
"/vulnerabilities",
|
||||||
|
].map((route) => ({
|
||||||
|
url: `https://vimfn.in${route}`,
|
||||||
|
lastModified: new Date().toISOString().split("T")[0],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...routes];
|
||||||
|
}
|
BIN
bun.lockb
Normal file
BIN
bun.lockb
Normal file
Binary file not shown.
17
components.json
Normal file
17
components.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
55
components/cards/testimonialCard.tsx
Normal file
55
components/cards/testimonialCard.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface TestimonialCard {
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
role?: string;
|
||||||
|
testimonial: string;
|
||||||
|
rating: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestimonialCard = ({
|
||||||
|
avatar,
|
||||||
|
name,
|
||||||
|
role,
|
||||||
|
testimonial,
|
||||||
|
rating,
|
||||||
|
}: TestimonialCard) => {
|
||||||
|
return (
|
||||||
|
<li className="inline-block w-full">
|
||||||
|
<div className="bg-primary/10 dark:bg-[#1c1c1c] mx-auto mb-5 flex w-full cursor-default flex-col gap-4 rounded-2xl px-[30px] py-6 shadow-md transition-all hover:scale-[103%]">
|
||||||
|
<div className="flex flex-row items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
src={`/testimonials/${avatar}.webp`}
|
||||||
|
alt="avatar1"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="small-semibold dark:text-white">{name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{role}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="body-regular dark:text-white">{testimonial}</p>
|
||||||
|
<div className="hue-rotate-90 text-lg">
|
||||||
|
{/* <Image
|
||||||
|
src="/testimonials/stars.svg"
|
||||||
|
alt="star"
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
className="object-cover"
|
||||||
|
/> */}
|
||||||
|
{"⭐".repeat(rating)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestimonialCard;
|
42
components/shared/About.tsx
Normal file
42
components/shared/About.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
import React from "react";
|
||||||
|
import Statistics from "./Statistics";
|
||||||
|
|
||||||
|
const About = () => {
|
||||||
|
return (
|
||||||
|
<section id="about" className="container py-2 sm:py-9">
|
||||||
|
<div className="bg-accent/50 border rounded-lg py-12">
|
||||||
|
<div className="px-6 flex flex-col-reverse md:flex-row gap-8 md:gap-12">
|
||||||
|
<Image
|
||||||
|
src="/about.svg"
|
||||||
|
alt="aboutpicture"
|
||||||
|
width={300}
|
||||||
|
height={300}
|
||||||
|
className="w-[300px] object-contain rounded-lg flex-shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<div className="pb-6">
|
||||||
|
<h2 className="text-3xl md:text-5xl font-bold">
|
||||||
|
About{" "}
|
||||||
|
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||||
|
SVRJS!
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-muted-foreground mt-4">
|
||||||
|
Host a webpage, run server-side JavaScript, use mods to expand
|
||||||
|
server functionality, or use it as a forward or reverse proxy.
|
||||||
|
SVRJS is a web server that runs on top of Node.JS, enabling
|
||||||
|
server-side JS on webpages. SVRJS also has an integrated log
|
||||||
|
viewer, log highlighter, and user management tool.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Statistics />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
11
components/shared/DataTable.tsx
Normal file
11
components/shared/DataTable.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const DataTable = () => {
|
||||||
|
return (
|
||||||
|
<section id="datatable" className="wrapper container py-2 sm:py-9">
|
||||||
|
DataTable
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataTable;
|
39
components/shared/FAQ.tsx
Normal file
39
components/shared/FAQ.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { questions } from "@/constants";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "../ui/accordion";
|
||||||
|
|
||||||
|
const Faq = () => {
|
||||||
|
return (
|
||||||
|
<section id="faq" className="wrapper container py-24 md:py-28">
|
||||||
|
<h2 className="text-3xl md:text-5xl font-bold mb-4 md:mb-2 md:pb-2 text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
Frequently Asked Question
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-muted-foreground text-start mt-4 md:mt-2 mb-8">
|
||||||
|
Find answers to common questions about SVRJS
|
||||||
|
</p>
|
||||||
|
<Accordion
|
||||||
|
type="single"
|
||||||
|
collapsible={true}
|
||||||
|
className="w-full AccordionRoot"
|
||||||
|
>
|
||||||
|
{questions.map(({ question, answer, key }) => (
|
||||||
|
<AccordionItem key={key} value={key} className="border-b">
|
||||||
|
<AccordionTrigger className="text-left text-lg">
|
||||||
|
{question}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="text-[1rem] text-muted-foreground">
|
||||||
|
{answer}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Faq;
|
107
components/shared/Footer.tsx
Normal file
107
components/shared/Footer.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
|
import Iconss from "../ui/icons";
|
||||||
|
import { FOOTERLINKS } from "@/constants";
|
||||||
|
import Logo from "./Logo";
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<footer className="flex flex-col w-full transition-all bg-zinc-50 text-black dark:bg-[#0308033b] border-t dark:text-white">
|
||||||
|
<div className="px-6 md:px-12 lg:px-24 py-10 w-full mx-auto max-w-screen-2xl">
|
||||||
|
<div className="flex flex-col lg:flex-row justify-between max-lg:items-start max-md:items-center items-center mb-6 ">
|
||||||
|
<div className="flex items-center mb-6 lg:mb-0">
|
||||||
|
<Logo width={200} height={80} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center md:items-start md:flex-row justify-between w-full lg:w-auto space-y-6 md:space-y-0 md:space-x-6 xl:space-x-16">
|
||||||
|
<div className="flex flex-col items-center md:items-start">
|
||||||
|
<div className="text-2xl font-light text-primary">
|
||||||
|
Quick Links
|
||||||
|
</div>
|
||||||
|
{FOOTERLINKS.otherPages.map((link) => (
|
||||||
|
<span key={link.href}>
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center md:items-start">
|
||||||
|
<h1 className="text-2xl font-light text-primary">Resources</h1>
|
||||||
|
{FOOTERLINKS.plans.map((link) => (
|
||||||
|
<span key={link.href}>
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center md:items-start">
|
||||||
|
<div className="text-2xl font-light text-primary">
|
||||||
|
Additional
|
||||||
|
</div>
|
||||||
|
{FOOTERLINKS.additional.map((link) => (
|
||||||
|
<span key={link.href}>
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center md:items-start">
|
||||||
|
<div className="text-2xl font-light text-primary">Social</div>
|
||||||
|
<p className="text-base font-light">
|
||||||
|
{FOOTERLINKS.social.supportText}
|
||||||
|
</p>
|
||||||
|
<div className="flex space-x-1 py-3">
|
||||||
|
<Iconss />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-t mb-6 border-gray-300 dark:border-white/30"></div>
|
||||||
|
<div className="flex flex-col lg:flex-row justify-between items-center space-y-4 lg:space-y-0 px-4">
|
||||||
|
<span className="text-sm font-light">
|
||||||
|
Copyright © 2023-{currentYear}{" "}
|
||||||
|
<Link
|
||||||
|
href={FOOTERLINKS.footerBottom.rightsReserved.href}
|
||||||
|
className="text-primary font-semibold"
|
||||||
|
>
|
||||||
|
{FOOTERLINKS.footerBottom.rightsReserved.label}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-light">
|
||||||
|
<Link
|
||||||
|
href={FOOTERLINKS.footerBottom.termsofService.href}
|
||||||
|
className="text-primary font-medium transition-all underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
{FOOTERLINKS.footerBottom.termsofService.label}{" "}
|
||||||
|
</Link>
|
||||||
|
|{" "}
|
||||||
|
<Link
|
||||||
|
href={FOOTERLINKS.footerBottom.privacyPolicy.href}
|
||||||
|
className="text-primary font-medium transition-all underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
{FOOTERLINKS.footerBottom.privacyPolicy.label}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
170
components/shared/Hero.tsx
Normal file
170
components/shared/Hero.tsx
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import HeroCards from "./HeroCards";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Check, Clipboard } from "lucide-react";
|
||||||
|
import GridPattern from "../ui/grid-pattern";
|
||||||
|
import AnimatedGradientText from "../ui/animated-gradient-text";
|
||||||
|
import { Happy_Monkey } from "next/font/google";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
const happyMonkey = Happy_Monkey({
|
||||||
|
preload: true,
|
||||||
|
weight: ["400"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Hero = () => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const [command, setCommand] = useState(
|
||||||
|
"curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20240509.sh > /tmp/installer.sh && sudo bash /tmp/installer.sh"
|
||||||
|
);
|
||||||
|
const [selectedButton, setSelectedButton] = useState<"linux" | "docker">(
|
||||||
|
"linux"
|
||||||
|
);
|
||||||
|
|
||||||
|
const commands = {
|
||||||
|
linux:
|
||||||
|
"curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20240509.sh > /tmp/installer.sh && sudo bash /tmp/installer.sh",
|
||||||
|
docker:
|
||||||
|
"docker pull svrjs/svrjs && docker run --name mysvrjs -d -p 80:80 --restart=always svrjs/svrjs",
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(command);
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonClick = (type: "linux" | "docker") => {
|
||||||
|
setCommand(commands[type]);
|
||||||
|
setSelectedButton(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative sm:container grid lg:grid-cols-2 place-items-center py-20 md:py-24 gap-10">
|
||||||
|
<GridPattern
|
||||||
|
className={cn(
|
||||||
|
"[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]",
|
||||||
|
"inset-x-0 inset-y-[-50%] h-[200%] opacity-30"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="text-center lg:text-start space-y-6">
|
||||||
|
<AnimatedGradientText className="mx-auto lg:mx-0">
|
||||||
|
🎉{" "}
|
||||||
|
<hr className="mx-2 h-4 w-[1px] shrink-0 bg-black dark:bg-gray-300" />
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
`inline animate-gradient bg-gradient-to-r from-[#235b1a] to-[#315620] dark:bg-gradient-to-r dark:from-[#6df458] dark:to-[#4c932a] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Expanding server functionality
|
||||||
|
</span>
|
||||||
|
</AnimatedGradientText>
|
||||||
|
<main className="text-5xl md:text-6xl font-bold">
|
||||||
|
<h1 className="inline custom-title">
|
||||||
|
<span className="text-transparent bg-gradient-to-r from-green-300 to-primary bg-clip-text">
|
||||||
|
Simplify
|
||||||
|
</span>{" "}
|
||||||
|
your server logic performance
|
||||||
|
</h1>
|
||||||
|
</main>
|
||||||
|
<p className="text-lg text-muted-foreground md:w-10/12 mx-auto lg:mx-0">
|
||||||
|
SVR.JS is a web server that runs on top of Node.JS, thus enabling
|
||||||
|
server-side JavaScript on webpages. SVR.JS also has an integrated log
|
||||||
|
viewer and more...
|
||||||
|
</p>
|
||||||
|
<div className="relative mx-auto lg:mx-0 flex gap-2 flex-col-reverse lg:flex-row justify-start items-center w-fit">
|
||||||
|
<Button
|
||||||
|
className="w-fit"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
variant={!isCopied ? "secondary" : "secondary"}
|
||||||
|
>
|
||||||
|
{!isCopied ? (
|
||||||
|
<Clipboard className="w-4 h-4 mr-2" />
|
||||||
|
) : (
|
||||||
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{command.slice(0, 39)}...
|
||||||
|
</Button>
|
||||||
|
<p className="hidden lg:block">|</p>
|
||||||
|
<p className="block lg:hidden">or</p>
|
||||||
|
<Link className="w-full" href="/downloads">
|
||||||
|
<Button className="w-full">Download</Button>
|
||||||
|
</Link>
|
||||||
|
<div className="pointer-events-none dark:invert -scale-x-100 absolute -bottom-14 max-lg:left-0 lg:right-20 inline-flex justify-center items-center gap-1">
|
||||||
|
<Image
|
||||||
|
src="/curly-arrow.png"
|
||||||
|
width={35}
|
||||||
|
height={35}
|
||||||
|
alt="Curly arrow"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
`mt-10 font-bold text-black -scale-x-100 text-sm ${happyMonkey.className}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Try Now!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center lg:justify-start justify-center gap-3 w-full">
|
||||||
|
<Button
|
||||||
|
className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${
|
||||||
|
selectedButton === "linux"
|
||||||
|
? "bg-accent"
|
||||||
|
: "bg-primary-foreground/20"
|
||||||
|
}`}
|
||||||
|
variant={"ghost"}
|
||||||
|
onClick={() => handleButtonClick("linux")}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Linux</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${
|
||||||
|
selectedButton === "docker"
|
||||||
|
? "bg-accent"
|
||||||
|
: "bg-primary-foreground/20"
|
||||||
|
}`}
|
||||||
|
variant={"ghost"}
|
||||||
|
onClick={() => handleButtonClick("docker")}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Docker</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
viewBox="0 0 50 50"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M27.336 23.076h-5.164v-5.8h5.164v5.8Zm0-19.951h-5.164v5.928h5.164V3.125Zm6.11 14.14H28.28v5.801h5.164v-5.8Zm-12.212-7.04H16.07v5.869h5.164v-5.87Zm6.102 0h-5.164v5.869h5.164v-5.87ZM48.96 19.99c-1.125-.947-3.719-1.289-5.711-.82-.258-2.344-1.305-4.385-3.211-6.22l-1.094-.909-.726 1.367c-1.438 2.715-1.828 7.188-.29 10.137-.679.459-2.015 1.084-3.78 1.045H.186c-.68 4.96.454 11.406 3.438 15.83 2.898 4.287 7.242 6.465 12.922 6.465 12.297 0 21.398-7.08 25.656-19.942 1.672.04 5.281.01 7.133-4.414.117-.244.516-1.289.664-1.67l-1.04-.869ZM9.03 17.266H3.875v5.8h5.164v-5.8h-.008Zm6.102 0H9.969v5.8h5.164v-5.8Zm6.101 0H16.07v5.8h5.164v-5.8Zm-6.101-7.041H9.969v5.869h5.164v-5.87Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="z-10">
|
||||||
|
<HeroCards />
|
||||||
|
</div>
|
||||||
|
<div className="shadow"></div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Hero;
|
188
components/shared/HeroCards.tsx
Normal file
188
components/shared/HeroCards.tsx
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { BorderBeam } from "@/components/ui/border-beam";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Happy_Monkey } from "next/font/google";
|
||||||
|
import { ArchiveRestore, Headset, Infinity, LightbulbIcon } from "lucide-react";
|
||||||
|
import Iconss from "../ui/icons";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
const happyMonkey = Happy_Monkey({
|
||||||
|
preload: true,
|
||||||
|
weight: ["400"],
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const HeroCards = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const cards = {
|
||||||
|
aboutCard: {
|
||||||
|
description:
|
||||||
|
"Discover more about our services and offerings. We aim to provide the best experience for our users.",
|
||||||
|
socialLinks: {
|
||||||
|
x: "https://x.com/SVR_JS",
|
||||||
|
Mastodon: "https://mastodon.social/@svrjs",
|
||||||
|
Bluesky: "https://bsky.app/profile/svrjs.org",
|
||||||
|
Odysee: "https://odysee.com/@SVRJS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pricingCard: {
|
||||||
|
planName: "Pro Plan",
|
||||||
|
badgeTitle: "Popular",
|
||||||
|
pricePerMonth: "$0",
|
||||||
|
description:
|
||||||
|
"Get the best features and priority support with our Pro Plan.",
|
||||||
|
primaryButtonText: "Download SVR Now",
|
||||||
|
onPrimaryButtonClick: () => router.push("/downloads"),
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
title: "Unlimited Projects",
|
||||||
|
icons: <Infinity className="rounded-full" width={25} height={25} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Priority Support",
|
||||||
|
icons: (
|
||||||
|
<ArchiveRestore className="rounded-full" width={25} height={25} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Free Updates",
|
||||||
|
icons: <Headset className="rounded-full" width={25} height={25} />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
curlyText: "Best Value!",
|
||||||
|
},
|
||||||
|
serviceCard: {
|
||||||
|
title: "Our Services",
|
||||||
|
description:
|
||||||
|
"We offer a variety of services to cater to your needs, including web development, SEO, and more.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hidden lg:flex flex-row flex-wrap gap-8 relative w-[700px] h-[500px]">
|
||||||
|
{/* Twitter First Top left */}
|
||||||
|
<Card className="absolute w-[340px] -top-[15px] drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||||
|
<CardHeader className="flex flex-row items-center gap-4 pb-2">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarImage
|
||||||
|
alt={"twitteravatar"}
|
||||||
|
src={"https://github.com/shadcn.png"}
|
||||||
|
/>
|
||||||
|
<AvatarFallback>Proxy</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<CardTitle className="text-lg">Proxy</CardTitle>
|
||||||
|
<CardDescription>@proxyxd_s</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
Svrjs has the best server side rendering{" "}
|
||||||
|
<span className="text-sky-400">#SVRJSONTOP</span>
|
||||||
|
</CardContent>
|
||||||
|
<BorderBeam className="-z-10" />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Socials Second top right */}
|
||||||
|
<Card className="absolute right-[20px] top-4 w-80 flex flex-col justify-center items-center drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||||
|
<CardHeader className="flex justify-center items-center pb-2">
|
||||||
|
<CardTitle className="text-center">Socials</CardTitle>
|
||||||
|
<CardDescription className="font-medium text-primary"></CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="text-center text-muted-foreground pb-2">
|
||||||
|
<p>{cards.aboutCard.description}</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Iconss />
|
||||||
|
</CardFooter>
|
||||||
|
<BorderBeam className="-z-10" />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pricings Bottom left */}
|
||||||
|
<Card className="absolute top-[170px] left-[50px] w-72 drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
{cards.pricingCard.planName}
|
||||||
|
<Badge variant="secondary" className="text-sm text-primary">
|
||||||
|
{cards.pricingCard.badgeTitle}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
<div>
|
||||||
|
<span className="text-3xl font-bold">
|
||||||
|
{cards.pricingCard.pricePerMonth}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground"> /month</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardDescription>{cards.pricingCard.description}</CardDescription>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={cards.pricingCard.onPrimaryButtonClick}
|
||||||
|
>
|
||||||
|
{cards.pricingCard.primaryButtonText}
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<hr className="w-4/5 m-auto -mt-2 mb-4" />
|
||||||
|
<CardFooter className="flex">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{cards.pricingCard.features.map((benefit) => (
|
||||||
|
<span
|
||||||
|
key={benefit.title}
|
||||||
|
className="inline-flex justify-center items-center gap-x-3"
|
||||||
|
>
|
||||||
|
{benefit.icons}
|
||||||
|
<h3>{benefit.title}</h3>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
<BorderBeam className="-z-10" />
|
||||||
|
<div className="pointer-events-none dark:invert -scale-x-100 absolute w-36 top-[9.5rem] -left-[7.5rem] inline-flex justify-center items-center gap-1">
|
||||||
|
<Image
|
||||||
|
src="/curly-arrow.png"
|
||||||
|
width={35}
|
||||||
|
height={35}
|
||||||
|
alt="Curly arrow"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={happyMonkey.style}
|
||||||
|
className="mt-10 font-bold text-black -scale-x-100 text-sm"
|
||||||
|
>
|
||||||
|
{cards.pricingCard.curlyText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Service */}
|
||||||
|
<Card className="absolute w-[350px] -right-[10px] bottom-[75px] drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||||
|
<CardHeader className="space-y-1 flex md:flex-row justify-start items-start gap-4">
|
||||||
|
<div className="mt-1 bg-primary/20 p-1 rounded-2xl">
|
||||||
|
<LightbulbIcon className="fill-black dark:fill-[#F596D3]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle>{cards.serviceCard.title}</CardTitle>
|
||||||
|
<CardDescription className="text-md mt-2">
|
||||||
|
{cards.serviceCard.description}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<BorderBeam className="-z-10" />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeroCards;
|
35
components/shared/HowItWorks.tsx
Normal file
35
components/shared/HowItWorks.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { Features } from "@/constants";
|
||||||
|
import React from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||||
|
|
||||||
|
const HowItWorks = () => {
|
||||||
|
return (
|
||||||
|
<section className="container text-center py-12 sm:py-24">
|
||||||
|
<h2 className="text-3xl md:text-5xl font-bold">
|
||||||
|
Accelerate your{" "}
|
||||||
|
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||||
|
development
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className="md:w-3/4 mx-auto mt-4 mb-8 text-lg md:text-xl text-muted-foreground">
|
||||||
|
Beautifully designed components that you can copy and paste into your
|
||||||
|
apps. Accessible. Customizable. Open Source.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{Features.map(({ icon, title, description }) => (
|
||||||
|
<Card key={title} className="bg-muted/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="grid gap-4 place-items-center">
|
||||||
|
{icon}
|
||||||
|
{title}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>{description}</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HowItWorks;
|
94
components/shared/Logo.tsx
Normal file
94
components/shared/Logo.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SVGProps } from "react";
|
||||||
|
const Logo = (props: SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={194}
|
||||||
|
height={48}
|
||||||
|
viewBox="0 0 194 48"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M53.22 40.315c-2.31 0-4.38-.375-6.21-1.125s-3.3-1.86-4.41-3.33c-1.08-1.47-1.65-3.24-1.71-5.31h8.19c.12 1.17.525 2.07 1.215 2.7.69.6 1.59.9 2.7.9 1.14 0 2.04-.255 2.7-.765.66-.54.99-1.275.99-2.205 0-.78-.27-1.425-.81-1.935-.51-.51-1.155-.93-1.935-1.26-.75-.33-1.83-.705-3.24-1.125-2.04-.63-3.705-1.26-4.995-1.89-1.29-.63-2.4-1.56-3.33-2.79-.93-1.23-1.395-2.835-1.395-4.815 0-2.94 1.065-5.235 3.195-6.885 2.13-1.68 4.905-2.52 8.325-2.52 3.48 0 6.285.84 8.415 2.52 2.13 1.65 3.27 3.96 3.42 6.93H56.01c-.06-1.02-.435-1.815-1.125-2.385-.69-.6-1.575-.9-2.655-.9-.93 0-1.68.255-2.25.765-.57.48-.855 1.185-.855 2.115 0 1.02.48 1.815 1.44 2.385.96.57 2.46 1.185 4.5 1.845 2.04.69 3.69 1.35 4.95 1.98 1.29.63 2.4 1.545 3.33 2.745.93 1.2 1.395 2.745 1.395 4.635 0 1.8-.465 3.435-1.395 4.905-.9 1.47-2.22 2.64-3.96 3.51-1.74.87-3.795 1.305-6.165 1.305ZM98.68 8.41 87.475 40h-9.63L66.642 8.41h8.19l7.83 23.85 7.874-23.85h8.145ZM117.557 40l-6.57-11.925h-1.845V40h-7.695V8.41h12.915c2.49 0 4.605.435 6.345 1.305 1.77.87 3.09 2.07 3.96 3.6.87 1.5 1.305 3.18 1.305 5.04 0 2.1-.6 3.975-1.8 5.625-1.17 1.65-2.91 2.82-5.22 3.51l7.29 12.51h-8.685Zm-8.415-17.37h4.77c1.41 0 2.46-.345 3.15-1.035.72-.69 1.08-1.665 1.08-2.925 0-1.2-.36-2.145-1.08-2.835-.69-.69-1.74-1.035-3.15-1.035h-4.77v7.83Zm24.81 17.73c-1.35 0-2.46-.39-3.33-1.17-.84-.81-1.26-1.8-1.26-2.97 0-1.2.42-2.205 1.26-3.015.87-.81 1.98-1.215 3.33-1.215 1.32 0 2.4.405 3.24 1.215.87.81 1.305 1.815 1.305 3.015 0 1.17-.435 2.16-1.305 2.97-.84.78-1.92 1.17-3.24 1.17Zm28.45-31.95v21.51c0 3.33-.945 5.895-2.835 7.695-1.86 1.8-4.38 2.7-7.56 2.7-3.33 0-6-.945-8.01-2.835-2.01-1.89-3.015-4.575-3.015-8.055h7.65c0 1.32.27 2.325.81 3.015.54.66 1.32.99 2.34.99.93 0 1.65-.3 2.16-.9.51-.6.765-1.47.765-2.61V8.41h7.695Zm17.196 31.905c-2.31 0-4.38-.375-6.21-1.125s-3.3-1.86-4.41-3.33c-1.08-1.47-1.65-3.24-1.71-5.31h8.19c.12 1.17.525 2.07 1.215 2.7.69.6 1.59.9 2.7.9 1.14 0 2.04-.255 2.7-.765.66-.54.99-1.275.99-2.205 0-.78-.27-1.425-.81-1.935-.51-.51-1.155-.93-1.935-1.26-.75-.33-1.83-.705-3.24-1.125-2.04-.63-3.705-1.26-4.995-1.89-1.29-.63-2.4-1.56-3.33-2.79-.93-1.23-1.395-2.835-1.395-4.815 0-2.94 1.065-5.235 3.195-6.885 2.13-1.68 4.905-2.52 8.325-2.52 3.48 0 6.285.84 8.415 2.52 2.13 1.65 3.27 3.96 3.42 6.93h-8.325c-.06-1.02-.435-1.815-1.125-2.385-.69-.6-1.575-.9-2.655-.9-.93 0-1.68.255-2.25.765-.57.48-.855 1.185-.855 2.115 0 1.02.48 1.815 1.44 2.385.96.57 2.46 1.185 4.5 1.845 2.04.69 3.69 1.35 4.95 1.98 1.29.63 2.4 1.545 3.33 2.745.93 1.2 1.395 2.745 1.395 4.635 0 1.8-.465 3.435-1.395 4.905-.9 1.47-2.22 2.64-3.96 3.51-1.74.87-3.795 1.305-6.165 1.305Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M42.32 18.844V7.992h14.126v21.702H42.32Z"
|
||||||
|
style={{
|
||||||
|
fill: "#fefefe",
|
||||||
|
strokeWidth: 0.0189204,
|
||||||
|
}}
|
||||||
|
transform="matrix(.57654 0 0 .57653 -3.77 -.913)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M166.84 38.111a1.35 1.35 0 0 0-1.354 1.354v38.793c0 .75.604 1.353 1.354 1.353h12.418a1.35 1.35 0 0 0 1.353-1.353V56.916h14.467v21.342c0 .75.604 1.353 1.354 1.353h12.418a1.35 1.35 0 0 0 1.353-1.353V39.465a1.35 1.35 0 0 0-1.353-1.354h-29.592z"
|
||||||
|
style={{
|
||||||
|
fill: "#ff0",
|
||||||
|
fillOpacity: 1,
|
||||||
|
strokeWidth: 1,
|
||||||
|
}}
|
||||||
|
transform="matrix(.15254 0 0 .15255 -3.77 -.913)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M38.707 34.451c-.714 0-1.289.575-1.289 1.29v83.343c0 .714.575 1.289 1.289 1.289h175.285c.715 0 1.29-.575 1.29-1.289V35.74c0-.714-.575-1.289-1.29-1.289zm135.363 5.91c.256.012.515.042.774.09 2.254.423 4.443 2.624 4.869 4.895.44 2.35-1.137 5.122-3.54 6.213-3.283 1.491-7.147-.328-8.206-3.864-1.098-3.665 2.264-7.508 6.103-7.334zm13.762 0c.256.012.512.042.772.09 2.254.423 4.443 2.624 4.869 4.895.44 2.35-1.138 5.122-3.54 6.213-3.284 1.491-7.147-.328-8.206-3.864-1.099-3.665 2.265-7.508 6.105-7.334zm14.08 0c.256.012.513.042.772.09 2.254.423 4.443 2.624 4.869 4.895.44 2.35-1.137 5.122-3.54 6.213-3.284 1.491-7.147-.328-8.206-3.864-1.098-3.665 2.266-7.508 6.105-7.334zM187.52 66.88c3.495 0 6.08 2.518 6.08 5.922 0 2.339-1.172 4.243-3.268 5.312-2.15 1.097-5.214.686-7.086-.953-1.002-.877-1.807-2.819-1.807-4.363 0-3.4 2.586-5.918 6.08-5.918zm-13.45.043c.256.012.515.041.774.09 2.254.423 4.443 2.623 4.869 4.894.44 2.35-1.137 5.12-3.54 6.211-3.283 1.492-7.147-.325-8.206-3.861-1.098-3.665 2.264-7.509 6.103-7.334zm27.842 0c.256.012.513.041.772.09 2.254.423 4.443 2.623 4.869 4.894.44 2.35-1.137 5.12-3.54 6.211-3.284 1.492-7.147-.325-8.206-3.861-1.098-3.665 2.266-7.509 6.105-7.334zM173.76 93.76c3.496 0 6.08 2.516 6.08 5.92 0 2.339-1.172 4.244-3.268 5.314-2.15 1.097-5.214.684-7.086-.955-1.002-.877-1.806-2.819-1.806-4.363 0-3.4 2.585-5.916 6.08-5.916zm13.76 0c3.495 0 6.08 2.516 6.08 5.92 0 2.339-1.172 4.244-3.268 5.314-2.15 1.097-5.214.684-7.086-.955-1.002-.877-1.807-2.819-1.807-4.363 0-3.4 2.586-5.916 6.08-5.916zm14.08 0c3.495 0 6.08 2.516 6.08 5.92 0 2.339-1.172 4.244-3.268 5.314-2.15 1.097-5.214.684-7.086-.955-1.002-.877-1.806-2.819-1.806-4.363 0-3.4 2.585-5.916 6.08-5.916z"
|
||||||
|
style={{
|
||||||
|
fill: "#767776",
|
||||||
|
strokeWidth: 0.32,
|
||||||
|
}}
|
||||||
|
transform="matrix(.15254 0 0 .15255 -3.77 -.913)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M22.225 63.584c-9.433-.053-8.71-.002-10.19-.728-.65-.319-.921-.517-1.495-1.09-.574-.574-.773-.847-1.091-1.495-.758-1.543-.686 1.133-.686-25.473 0-26.606-.072-23.93.686-25.473.318-.649.517-.921 1.09-1.495.574-.573.847-.772 1.495-1.09 1.537-.755-.601-.687 21.325-.687 21.925 0 19.788-.068 21.324.686.649.319.921.518 1.495 1.091.573.574.772.846 1.09 1.495.758 1.543.686-1.133.686 25.473 0 26.606.072 23.93-.686 25.473-.318.648-.517.92-1.09 1.494-.574.574-.846.772-1.495 1.09-1.026.504-1.583.64-2.825.688-1.681.065-20.67.091-29.633.04zm32.564-34.386c.67-.314 1.129-.761 1.45-1.414.246-.5.276-.638.276-1.284 0-.645-.03-.783-.276-1.283-.321-.653-.78-1.1-1.45-1.414l-.475-.224H12.319l-.476.224a2.902 2.902 0 0 0-1.45 1.414c-.245.5-.275.638-.275 1.282 0 .6.037.8.22 1.19.39.832 1.015 1.382 1.878 1.652.292.092 4.135.11 21.227.096l20.87-.016zm0-7.112c.67-.314 1.129-.761 1.45-1.414.246-.5.276-.638.276-1.284 0-.645-.03-.783-.276-1.283-.321-.653-.78-1.1-1.45-1.414l-.475-.224H12.319l-.476.224a2.902 2.902 0 0 0-1.45 1.414c-.245.5-.275.638-.275 1.282 0 .6.037.8.22 1.19.39.832 1.015 1.382 1.878 1.652.292.092 4.135.11 21.227.096l20.87-.016zm0-7.027c.67-.314 1.129-.762 1.45-1.414.246-.5.276-.639.276-1.284s-.03-.784-.276-1.284c-.321-.652-.78-1.1-1.45-1.414l-.475-.223H12.319l-.476.223a2.91 2.91 0 0 0-1.45 1.414c-.245.5-.275.639-.275 1.282 0 .6.037.8.22 1.191.39.831 1.015 1.381 1.878 1.652.292.092 4.135.109 21.227.096l20.87-.016z"
|
||||||
|
style={{
|
||||||
|
fill: "#02f402",
|
||||||
|
strokeWidth: 0.0846667,
|
||||||
|
}}
|
||||||
|
transform="matrix(.57654 0 0 .57653 -3.77 -.913)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16.535 39.881a8.588 8.588 0 0 0-.417.013c-.557.03-1.098.109-1.393.224-.614.24-1.128.687-1.414 1.23-.202.384-.23.529-.23 1.24 0 .903.088 1.143.604 1.644.396.385.979.598 2.448.896 1.853.376 2.24.597 2.24 1.285 0 .772-.561 1.18-1.632 1.184-1.085.004-1.706-.323-1.932-1.015-.056-.171-.144-.34-.195-.374-.052-.034-.422-.063-.824-.064-.888-.002-.965.057-.837.631.276 1.23 1.081 1.974 2.483 2.295 1.172.268 2.833.01 3.675-.57 1.228-.844 1.52-2.847.56-3.837-.444-.456-1.039-.698-2.421-.985-1.651-.343-1.796-.387-2.09-.635-.215-.18-.258-.279-.258-.584 0-.683.523-1.056 1.481-1.056.89 0 1.464.327 1.673.954l.12.357h1.678l-.009-.474c-.02-1.09-.894-1.978-2.239-2.277-.27-.06-.659-.085-1.071-.082zm33.95 0a8.588 8.588 0 0 0-.416.013c-.557.03-1.097.109-1.392.224-.615.24-1.129.687-1.415 1.23-.202.384-.229.529-.23 1.24 0 .903.088 1.143.604 1.644.396.385.98.598 2.448.896 1.853.376 2.24.597 2.24 1.285 0 .772-.56 1.18-1.631 1.184-1.085.004-1.707-.323-1.933-1.015-.056-.171-.143-.34-.195-.374-.051-.034-.422-.063-.824-.064-.888-.002-.965.057-.836.631.276 1.23 1.08 1.974 2.482 2.295 1.172.268 2.833.01 3.676-.57 1.227-.844 1.52-2.847.559-3.837-.443-.456-1.038-.698-2.42-.985-1.652-.343-1.797-.387-2.092-.635-.214-.18-.258-.279-.258-.584 0-.683.525-1.056 1.483-1.056.89 0 1.463.327 1.672.954l.12.357h1.679l-.01-.474c-.02-1.09-.894-1.978-2.238-2.277-.27-.06-.66-.085-1.072-.082zm-5.446.081c-.657 0-.763.02-.84.164-.056.105-.088 1.368-.088 3.52v3.356l-.247.248c-.21.209-.315.247-.681.247-.645 0-.816-.205-.89-1.067a7.664 7.664 0 0 0-.091-.777c-.026-.082-.253-.103-.904-.085l-.87.025v.804c0 1.497.556 2.308 1.827 2.665.569.16 1.546.117 2.195-.094.333-.11.59-.277.903-.588.7-.696.706-.73.706-4.826 0-3.104-.015-3.495-.134-3.54-.074-.029-.473-.052-.886-.052zm-18.134.002c-.432 0-.83.03-.885.065-.055.035-.503 1.45-.995 3.145-.491 1.694-.913 3.1-.936 3.124-.024.024-.057.027-.074.008-.016-.02-.439-1.426-.94-3.125-.5-1.7-.953-3.12-1.006-3.155-.054-.035-.46-.054-.901-.042-.673.018-.808.044-.83.163-.037.188 2.774 8.522 2.925 8.673.08.08.329.117.786.117.603 0 .681-.018.787-.19.212-.345 2.957-8.52 2.906-8.654-.04-.105-.205-.13-.837-.129zm5.608.016a64.154 64.154 0 0 0-1.06.003l-2.455.022-.022 4.397c-.017 3.462.001 4.412.085 4.465.124.08 1.427.094 1.624.019.115-.045.134-.273.135-1.672 0-.891.034-1.672.073-1.735.057-.09.336-.107 1.29-.083 1.72.043 1.768.088 1.769 1.662 0 .545.04 1.132.087 1.305.145.522.25.575 1.14.575.435 0 .85-.024.924-.052.178-.068.17-.253-.023-.528-.124-.178-.17-.455-.216-1.325-.032-.606-.098-1.251-.146-1.435-.086-.323-.492-.818-.75-.913-.093-.035-.007-.155.32-.446.655-.585.815-.942.819-1.827.003-.644-.026-.782-.253-1.227a2.116 2.116 0 0 0-1.501-1.133c-.208-.043-.874-.069-1.84-.073zm-.85 1.594c.181 0 .404.004.676.01l1.372.03.247.277c.21.235.247.347.247.745 0 .55-.154.895-.463 1.036-.236.108-2.764.147-2.867.044-.083-.083-.066-1.913.019-2.048.043-.068.222-.095.768-.094zm5.844 5.495v.885c0 .629.031.897.106.927.177.072 1.538.059 1.655-.015.08-.05.102-.294.084-.91l-.025-.839-.91-.024z"
|
||||||
|
style={{
|
||||||
|
fill: "#000",
|
||||||
|
strokeWidth: 0.0846667,
|
||||||
|
}}
|
||||||
|
transform="matrix(.57654 0 0 .57653 -3.77 -.913)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M115.715 236.383a.602.602 0 0 0-.604.603v12.047l.014 1.498.086 9.03-.1.125v13.777h-62.84l-2.095.992a12.68 12.68 0 0 0-6.12 6.121c-.919 1.94-1.017 2.432-1.017 5.184 0 2.751.098 3.243 1.018 5.183a12.68 12.68 0 0 0 6.119 6.121l2.144 1.016 31.026.08 5.91.016h77.473l5.605-.016 29.426-.082 2.144-1.015a12.676 12.676 0 0 0 6.12-6.12c.919-1.94 1.015-2.432 1.015-5.183s-.096-3.245-1.016-5.186a12.676 12.676 0 0 0-6.119-6.119l-2.095-.992h-60.313v-36.477a.602.602 0 0 0-.603-.603z"
|
||||||
|
style={{
|
||||||
|
fill: "#999998",
|
||||||
|
strokeWidth: 0.32,
|
||||||
|
}}
|
||||||
|
transform="matrix(.15254 0 0 .15255 -3.77 -.913)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M42.415 280.972c-.581-.136-1.244-.507-1.76-.986-.558-.516-.824-.918-1.114-1.68-.184-.485-.19-.592-.186-3.422.005-3.115.04-3.468.427-4.285.26-.552 1.02-1.398 1.514-1.687.833-.488 1.247-.575 2.753-.575s1.92.087 2.753.575c.494.29 1.253 1.135 1.514 1.687.387.817.422 1.17.427 4.285.004 2.83-.002 2.937-.186 3.423-.29.761-.556 1.163-1.114 1.68-.533.493-1.171.845-1.795.988-.452.105-2.787.102-3.233-.003z"
|
||||||
|
style={{
|
||||||
|
fill: "#fd7f00",
|
||||||
|
strokeWidth: 0.0846667,
|
||||||
|
}}
|
||||||
|
transform="matrix(.57654 0 0 .57653 -9.65 -116.597)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M33.359 6.053c-21.926 0-19.788-.069-21.325.686-.648.319-.92.517-1.494 1.091s-.772.846-1.09 1.495c-.759 1.543-.687-1.133-.687 25.473 0 26.606-.072 23.93.686 25.473.319.648.517.92 1.09 1.494.575.574.846.772 1.495 1.09 1.48.727.758.676 10.191.729 8.964.05 27.952.024 29.633-.041 1.242-.048 1.799-.184 2.825-.687.649-.319.92-.517 1.495-1.09.573-.574.772-.847 1.09-1.495.758-1.543.686 1.133.686-25.473 0-26.606.072-23.93-.686-25.473-.318-.649-.517-.921-1.09-1.495-.574-.573-.847-.772-1.495-1.09-1.537-.755.602-.687-21.324-.687ZM12.319 9.44h41.995l.476.223a2.914 2.914 0 0 1 1.45 1.414c.245.5.275.639.275 1.284s-.03.784-.276 1.284c-.321.652-.78 1.1-1.45 1.414l-.475.223-20.87.016c-17.093.013-20.935-.005-21.228-.096-.863-.27-1.488-.82-1.878-1.652-.183-.39-.22-.59-.22-1.19 0-.644.03-.783.276-1.283.321-.652.78-1.1 1.45-1.414zm0 7.027h41.995l.476.224c.669.313 1.128.76 1.45 1.413.245.5.275.64.275 1.285 0 .645-.03.783-.276 1.283-.321.653-.78 1.1-1.45 1.414l-.475.223-20.87.016c-17.093.013-20.935-.005-21.228-.096-.863-.27-1.488-.82-1.878-1.651-.183-.391-.22-.591-.22-1.192 0-.643.03-.782.276-1.282a2.9 2.9 0 0 1 1.45-1.413zm0 7.113h41.995l.476.223c.669.314 1.128.761 1.45 1.414.245.5.275.638.275 1.283 0 .646-.03.784-.276 1.284-.321.653-.78 1.1-1.45 1.414l-.475.224-20.87.015c-17.093.013-20.935-.004-21.228-.096-.863-.27-1.488-.82-1.878-1.651-.183-.39-.22-.591-.22-1.191 0-.644.03-.783.276-1.282.321-.653.78-1.1 1.45-1.414zm20.955 8.833c6.13 0 12.259.046 12.83.139a12.478 12.478 0 0 1 6.152 2.89c2.046 1.798 3.473 4.41 3.923 7.185.165 1.02.165 3.726 0 4.746-.45 2.775-1.877 5.387-3.923 7.185a12.477 12.477 0 0 1-6.153 2.89c-1.089.177-24.692.17-25.74-.007-2.202-.372-4.402-1.417-6.071-2.883-2.046-1.798-3.473-4.41-3.922-7.185-.166-1.02-.166-3.726 0-4.746a12.476 12.476 0 0 1 2.89-6.153c1.797-2.046 4.41-3.473 7.184-3.923.571-.092 6.7-.138 12.83-.138zm-10.278 4.02c.062-.009.125 0 .188 0-.063 0-.126-.009-.188 0z"
|
||||||
|
style={{
|
||||||
|
fill: "#017801",
|
||||||
|
strokeWidth: 0.0846667,
|
||||||
|
}}
|
||||||
|
transform="matrix(.57654 0 0 .57653 -3.77 -.913)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16.417 9.252c-.072.018-.142.04-.213.061h.12l.105-.034zm-5.403.16c.132.019.265.028.398.026v-.003a14.897 14.897 0 0 1-.398-.023Zm4.417 1.033-.233.027-.233.027-.023 1.714-.023 1.715h.512v-1.525h1.778v1.525h.422v-3.472h-.422v1.524H15.43v-.767zm2.625.011v.423h1.1v3.049h.508v-3.049h1.101v-.423H19.41Zm2.878 0v.423h1.1v3.049h.509v-3.049h1.1v-.423H22.29Zm3.217 0v3.472h.424v-1.44h1.286l.276-.13.276-.131.116-.217.116-.218v-.64l-.116-.217-.116-.217-.276-.131-.276-.13h-.854zm.424.423h1.24l.184.197.184.196v.442l-.222.176-.224.175h-1.162v-.593zm-9.144 6.593-.233.027-.233.027-.023 1.715-.023 1.714h.512V19.43h1.778v1.524h.422v-3.471h-.422v1.524H15.43v-.768zm2.625.012v.423h1.1v3.048h.508v-3.048h1.101v-.423H19.41Zm2.878 0v.423h1.1v3.048h.509v-3.048h1.1v-.423H22.29Zm3.217 0v3.471h.424v-1.44h1.286l.276-.13.276-.13.116-.218.116-.217v-.64l-.116-.218-.116-.217-.276-.13-.276-.131h-.854zm.424.423h1.24l.184.196.184.196v.442l-.222.176-.224.175h-1.162V18.5Zm-9.144 6.677-.233.027-.233.027-.023 1.714-.023 1.715h.512v-1.524h1.778v1.524h.422v-3.472h-.422v1.525H15.43v-.768zm2.625.011v.424h1.1v3.048h.508v-3.048h1.101v-.424H19.41Zm2.878 0v.424h1.1v3.048h.509v-3.048h1.1v-.424H22.29Zm3.217 0v3.472h.424v-1.44h1.286l.276-.13.276-.131.116-.217.116-.217v-.64l-.116-.218-.116-.217-.276-.13-.276-.132h-.854zm.424.424h1.24l.184.196.184.196v.442l-.222.176-.224.175h-1.162v-.592z"
|
||||||
|
style={{
|
||||||
|
fill: "#666",
|
||||||
|
fillOpacity: 1,
|
||||||
|
strokeWidth: 0.0846667,
|
||||||
|
}}
|
||||||
|
transform="matrix(.57654 0 0 .57653 -3.77 -.913)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Logo;
|
76
components/shared/MobileNav.tsx
Normal file
76
components/shared/MobileNav.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "../ui/sheet";
|
||||||
|
import { Menu } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ThemeToggle from "../ui/theme-toggle";
|
||||||
|
import { NAVBAR } from "@/constants";
|
||||||
|
import { buttonVariants } from "../ui/button";
|
||||||
|
import Logo from "./Logo";
|
||||||
|
|
||||||
|
const MobileNav = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex md:hidden">
|
||||||
|
<ThemeToggle />
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger>
|
||||||
|
<span className="sr-only">Menu</span>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>
|
||||||
|
<Logo width={120} height={40} />
|
||||||
|
</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<nav
|
||||||
|
id="mobile-menu"
|
||||||
|
className="flex flex-col justify-center items-center gap-2 mt-4"
|
||||||
|
>
|
||||||
|
{NAVBAR.centerLinks?.map(({ href = "", label, target }) => (
|
||||||
|
<Link
|
||||||
|
key={label}
|
||||||
|
href={href}
|
||||||
|
target={target}
|
||||||
|
className={buttonVariants({ variant: "ghost" })}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{NAVBAR.rightLinks?.map(({ href, label, target }) => (
|
||||||
|
<Link
|
||||||
|
key={label}
|
||||||
|
href={href}
|
||||||
|
target={target}
|
||||||
|
className={`w-[110px] gap-2 ${buttonVariants({
|
||||||
|
variant: "secondary",
|
||||||
|
})}`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={25}
|
||||||
|
height={25}
|
||||||
|
viewBox="0 0 123 123"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M120.208 55.953 66.715 2.463a7.885 7.885 0 0 0-11.158 0l-11.109 11.11 14.088 14.088a9.373 9.373 0 0 1 11.87 11.948l13.578 13.579a9.368 9.368 0 0 1 9.704 2.23 9.386 9.386 0 0 1-6.64 16.025 9.393 9.393 0 0 1-9.21-7.547 9.384 9.384 0 0 1 .526-5.416L65.697 45.817v33.33a9.385 9.385 0 0 1 2.48 15.053 9.386 9.386 0 0 1-15.311-3.046A9.388 9.388 0 0 1 54.9 80.923a9.378 9.378 0 0 1 3.078-2.052V45.235a9.336 9.336 0 0 1-3.078-2.047A9.4 9.4 0 0 1 52.88 32.92l-13.89-13.89L2.311 55.703a7.89 7.89 0 0 0 0 11.16l53.495 53.497a7.895 7.895 0 0 0 11.157 0l53.244-53.245a7.9 7.9 0 0 0 0-11.162Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileNav;
|
90
components/shared/Navbar.tsx
Normal file
90
components/shared/Navbar.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuList,
|
||||||
|
} from "@radix-ui/react-navigation-menu";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ThemeToggle from "../ui/theme-toggle";
|
||||||
|
import { NAVBAR } from "@/constants";
|
||||||
|
import { buttonVariants } from "../ui/button";
|
||||||
|
import MobileNav from "./MobileNav";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import Logo from "./Logo";
|
||||||
|
|
||||||
|
const Navbar = () => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
return (
|
||||||
|
<header className="sticky border-b top-0 z-40 w-full shadow-md bg-white dark:border-b-slate-800 dark:bg-background">
|
||||||
|
{/* LOGO LEFT NAVBAR */}
|
||||||
|
<NavigationMenu className="mx-auto">
|
||||||
|
<NavigationMenuList className="container h-16 px-4 w-full flex justify-between items-center">
|
||||||
|
<NavigationMenuItem className="font-bold flex items-center">
|
||||||
|
<Link href="/#" className="inline-flex items-center gap-2">
|
||||||
|
<span className="sr-only">SVR.JS</span>
|
||||||
|
<Logo width={120} height={40} />
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
{/* Mobile view */}
|
||||||
|
<NavigationMenuItem className="flex md:hidden">
|
||||||
|
<MobileNav />
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
{/* Desktop Menu */}
|
||||||
|
<NavigationMenuItem className="hidden md:flex">
|
||||||
|
<nav className="hidden md:flex gap-4">
|
||||||
|
{NAVBAR.centerLinks?.map(({ href, label, target }) => (
|
||||||
|
<Link
|
||||||
|
key={label}
|
||||||
|
href={href}
|
||||||
|
target={target}
|
||||||
|
className={`text-[17px] tracking-tight ${
|
||||||
|
pathname == href ? "bg-muted-foreground/20" : ""
|
||||||
|
} ${buttonVariants({
|
||||||
|
variant: "ghost",
|
||||||
|
})}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem className="hidden md:flex gap-2 items-center">
|
||||||
|
{NAVBAR.rightLinks?.map(({ href = "", label, target }) => (
|
||||||
|
<Link
|
||||||
|
key={label}
|
||||||
|
href={href}
|
||||||
|
target={target}
|
||||||
|
className={`border ${buttonVariants({
|
||||||
|
variant: "ghost",
|
||||||
|
size: "icon",
|
||||||
|
})}`}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Git</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={25}
|
||||||
|
height={25}
|
||||||
|
viewBox="0 0 123 123"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M120.208 55.953 66.715 2.463a7.885 7.885 0 0 0-11.158 0l-11.109 11.11 14.088 14.088a9.373 9.373 0 0 1 11.87 11.948l13.578 13.579a9.368 9.368 0 0 1 9.704 2.23 9.386 9.386 0 0 1-6.64 16.025 9.393 9.393 0 0 1-9.21-7.547 9.384 9.384 0 0 1 .526-5.416L65.697 45.817v33.33a9.385 9.385 0 0 1 2.48 15.053 9.386 9.386 0 0 1-15.311-3.046A9.388 9.388 0 0 1 54.9 80.923a9.378 9.378 0 0 1 3.078-2.052V45.235a9.336 9.336 0 0 1-3.078-2.047A9.4 9.4 0 0 1 52.88 32.92l-13.89-13.89L2.311 55.703a7.89 7.89 0 0 0 0 11.16l53.495 53.497a7.895 7.895 0 0 0 11.157 0l53.244-53.245a7.9 7.9 0 0 0 0-11.162Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<ThemeToggle />
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
77
components/shared/Newsletter.tsx
Normal file
77
components/shared/Newsletter.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Happy_Monkey } from "next/font/google";
|
||||||
|
|
||||||
|
const happyMonkey = Happy_Monkey({
|
||||||
|
preload: true,
|
||||||
|
weight: "400",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Newsletter = () => {
|
||||||
|
const [submission, setSubmission] = useState<
|
||||||
|
"idle" | "loading" | "success" | "error"
|
||||||
|
>("idle");
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
console.log("Done");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="newsletter">
|
||||||
|
<hr className="w-11/12 mx-auto" />
|
||||||
|
<div className="container py-24 md:py-32">
|
||||||
|
<h3 className="text-center text-4xl md:text-5xl md:pb-2 text-black font-bold dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
Join The Newsletter!
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg text-muted-foreground text-center mt-4 md:mt-2 mb-8">
|
||||||
|
Choosing the right website deployment option is important when
|
||||||
|
creating a website, because it directly impacts the user experience
|
||||||
|
and the resources required to run your website.
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
className="relative flex flex-col w-full md:flex-row md:w-6/12 lg:w-4/12 mx-auto gap-4 md:gap-2"
|
||||||
|
aria-label="Email Information"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<Input placeholder="example@subscribe.com"></Input>
|
||||||
|
|
||||||
|
<Button disabled={submission === "loading"}>Subscribe</Button>
|
||||||
|
<div className="pointer-events-none dark:invert -scale-x-100 absolute -bottom-14 right-1/2 md:right-14 inline-flex justify-center items-center gap-1">
|
||||||
|
<Image
|
||||||
|
src="/curly-arrow.png"
|
||||||
|
alt="see here"
|
||||||
|
width={35}
|
||||||
|
height={35}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`}
|
||||||
|
>
|
||||||
|
{submission === "idle" && "Subscribe Now"}
|
||||||
|
{submission === "loading" && (
|
||||||
|
<p className="text-sm text-center">Subscribing...</p>
|
||||||
|
)}
|
||||||
|
{submission === "success" && (
|
||||||
|
<p className="dark:invert text-sm text-center text-green-500">
|
||||||
|
🎉 Subscribed successfully...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{submission === "error" && (
|
||||||
|
<p className="dark:invert text-sm text-center text-red-500">
|
||||||
|
😥 Something went wrong...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<hr className="w-11/12 mx-auto" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Newsletter;
|
49
components/shared/Partners.tsx
Normal file
49
components/shared/Partners.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ArrowUpRight } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
const Partners = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const handleClick = () => {
|
||||||
|
router.push("/docs");
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="partners"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-4 flex flex-col"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-5xl font-bold text-start">
|
||||||
|
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||||
|
SVRJS
|
||||||
|
</span>{" "}
|
||||||
|
in action
|
||||||
|
</h2>
|
||||||
|
<div className="w-full flex-start flex-row">
|
||||||
|
<div className="flex max-md:flex-col items-center justify-start gap-4">
|
||||||
|
<h2 className="text-md font-medium bg-accent/60 px-2 py-2 rounded-md">
|
||||||
|
Process of setting up a WordPress website running on SVR.JS.
|
||||||
|
</h2>
|
||||||
|
<Button
|
||||||
|
onClick={handleClick}
|
||||||
|
className="flex-center font-bold max-md:w-full max-w-xl"
|
||||||
|
>
|
||||||
|
Docs <ArrowUpRight />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<video
|
||||||
|
src="/svgaction.mp4"
|
||||||
|
className="rounded-xl aspect-video bg-[#09090b]"
|
||||||
|
controls
|
||||||
|
poster="/poster.svg"
|
||||||
|
></video>
|
||||||
|
<hr className="w-full h-1" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Partners;
|
0
components/shared/Sidebar.tsx
Normal file
0
components/shared/Sidebar.tsx
Normal file
21
components/shared/Statistics.tsx
Normal file
21
components/shared/Statistics.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { stats } from "@/constants";
|
||||||
|
import NumberTicker from "../widgets/num-tick";
|
||||||
|
|
||||||
|
const Statistics = () => {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{stats.map(({ title, count }) => (
|
||||||
|
<div key={title} className="space-y-2 text-center">
|
||||||
|
<h2 className="text-3xl sm:text-4xl font-bold">
|
||||||
|
<NumberTicker value={count} />
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl text-muted-foreground">{title}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Statistics;
|
105
components/shared/Testimonials.tsx
Normal file
105
components/shared/Testimonials.tsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import React from "react";
|
||||||
|
import TestimonialCard from "../cards/testimonialCard";
|
||||||
|
|
||||||
|
const testimonials = [
|
||||||
|
{
|
||||||
|
name: "John Doe",
|
||||||
|
role: "CEO, Example Corp.",
|
||||||
|
avatar: "avatar1",
|
||||||
|
testimonial:
|
||||||
|
"Working with this team was a fantastic experience. They developed our website exactly to our specifications, and everything was seamless and well-integrated.",
|
||||||
|
rating: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jane Smith",
|
||||||
|
role: "CEO, CleanCo",
|
||||||
|
avatar: "avatar2",
|
||||||
|
testimonial:
|
||||||
|
"We're thrilled with the website. It's simple, clean, and has significantly boosted our sales. The developers did an excellent job.",
|
||||||
|
rating: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sam Green",
|
||||||
|
role: "Web Developer",
|
||||||
|
avatar: "avatar3",
|
||||||
|
testimonial:
|
||||||
|
"Collaborating with this team to build a SaaS-integrated website was a perfect experience. I look forward to working with them again.",
|
||||||
|
rating: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chris Brown",
|
||||||
|
role: "Web Coder",
|
||||||
|
avatar: "avatar4",
|
||||||
|
testimonial:
|
||||||
|
"The team's understanding of our needs and their ability to provide fitting solutions was impressive. Their support and guidance were invaluable.",
|
||||||
|
rating: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alex Johnson",
|
||||||
|
avatar: "avatar5",
|
||||||
|
testimonial:
|
||||||
|
"Exceptional service and outstanding results. They consistently deliver on time and within budget, making them our go-to partner for all our projects.",
|
||||||
|
rating: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Patricia Taylor",
|
||||||
|
role: "Web Developer",
|
||||||
|
avatar: "avatar6",
|
||||||
|
testimonial:
|
||||||
|
"It was great to work with them. I needed a design for a SaaS project, and it was delivered within 2 days.",
|
||||||
|
rating: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Emily Davis",
|
||||||
|
role: "UX Designer, Creative Agency",
|
||||||
|
avatar: "avatar7",
|
||||||
|
testimonial:
|
||||||
|
"Collaborating with them has been a pleasure. Their creativity and user-centric approach have significantly enhanced our product's usability.",
|
||||||
|
rating: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Michael Lee",
|
||||||
|
avatar: "avatar8",
|
||||||
|
testimonial:
|
||||||
|
"They have a keen understanding of our business needs and consistently deliver top-notch solutions. Their reliability and efficiency are commendable.",
|
||||||
|
rating: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sarah Wilson",
|
||||||
|
avatar: "avatar9",
|
||||||
|
testimonial:
|
||||||
|
"Their dedication to client satisfaction is evident in everything they do. We've seen remarkable improvements in our processes thanks to their expertise.",
|
||||||
|
rating: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Testimonials = () => {
|
||||||
|
return (
|
||||||
|
<section className="mx-auto flex w-full max-w-7xl flex-col pt-12 md:pt-24">
|
||||||
|
<div className="flex flex-row items-center justify-center space-x-1">
|
||||||
|
<span className="text-white/50 text-xs lg:text-base">Testimonials</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-center">
|
||||||
|
Hear it from{" "}
|
||||||
|
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||||
|
our users
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<ul className="wrapper columns-1 gap-5 md:columns-2 lg:columns-3 py-6 mt-6">
|
||||||
|
{testimonials.map((testimonial, idx) => (
|
||||||
|
<TestimonialCard
|
||||||
|
avatar={testimonial.avatar}
|
||||||
|
name={testimonial.name}
|
||||||
|
role={testimonial.role}
|
||||||
|
testimonial={testimonial.testimonial}
|
||||||
|
rating={testimonial.rating}
|
||||||
|
key={idx}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Testimonials;
|
10
components/shared/providers/AuthProvider.tsx
Normal file
10
components/shared/providers/AuthProvider.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
"use client";
|
||||||
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
|
||||||
|
export default function AuthProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <SessionProvider>{children}</SessionProvider>;
|
||||||
|
}
|
17
components/shared/providers/changelogLayout.tsx
Normal file
17
components/shared/providers/changelogLayout.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface ChangelogLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangelogLayout: React.FC<ChangelogLayoutProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||||
|
<div className="prose max-w-full prose-lg dark:prose-invert">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
9
components/shared/providers/themeprovider.tsx
Normal file
9
components/shared/providers/themeprovider.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
58
components/ui/accordion.tsx
Normal file
58
components/ui/accordion.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
|
import { ChevronDown, PlusCircleIcon, PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root;
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AccordionItem.displayName = "AccordionItem";
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-45",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<PlusIcon className="h-5 w-5 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
));
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
));
|
||||||
|
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
25
components/ui/animated-gradient-text.tsx
Normal file
25
components/ui/animated-gradient-text.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export default function AnimatedGradientText({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`direction-reverse absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#26751a] to-[#154300] dark:bg-gradient-to-r dark:from-[#aafc9e]/80 dark:to-[#97fd67]/30 bg-[length:var(--bg-size)_100%] p-[1px] ![mask-composite:subtract] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
39
components/ui/animated-shiny-text.tsx
Normal file
39
components/ui/animated-shiny-text.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CSSProperties, FC, ReactNode } from "react";
|
||||||
|
|
||||||
|
interface AnimatedShinyTextProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
shimmerWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
shimmerWidth = 100,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--shimmer-width": `${shimmerWidth}px`,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50 ",
|
||||||
|
|
||||||
|
// Shimmer effect
|
||||||
|
"animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
|
||||||
|
|
||||||
|
// Shimmer gradient
|
||||||
|
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
|
||||||
|
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnimatedShinyText;
|
50
components/ui/avatar.tsx
Normal file
50
components/ui/avatar.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Avatar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
ref={ref}
|
||||||
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback };
|
36
components/ui/badge.tsx
Normal file
36
components/ui/badge.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
49
components/ui/border-beam.tsx
Normal file
49
components/ui/border-beam.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface BorderBeamProps {
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
duration?: number;
|
||||||
|
borderWidth?: number;
|
||||||
|
anchor?: number;
|
||||||
|
colorFrom?: string;
|
||||||
|
colorTo?: string;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BorderBeam = ({
|
||||||
|
className,
|
||||||
|
size = 200,
|
||||||
|
duration = 15,
|
||||||
|
anchor = 90,
|
||||||
|
borderWidth = 1.5,
|
||||||
|
colorFrom = "#8803AF",
|
||||||
|
colorTo = "#61DAFB",
|
||||||
|
delay = 0,
|
||||||
|
}: BorderBeamProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--size": size,
|
||||||
|
"--duration": duration,
|
||||||
|
"--anchor": anchor,
|
||||||
|
"--border-width": borderWidth,
|
||||||
|
"--color-from": colorFrom,
|
||||||
|
"--color-to": colorTo,
|
||||||
|
"--delay": `-${delay}s`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-[0] rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent] opacity-10",
|
||||||
|
|
||||||
|
// mask styles
|
||||||
|
"![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",
|
||||||
|
|
||||||
|
// pseudo styles
|
||||||
|
"after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
56
components/ui/button.tsx
Normal file
56
components/ui/button.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
86
components/ui/card.tsx
Normal file
86
components/ui/card.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
122
components/ui/dialog.tsx
Normal file
122
components/ui/dialog.tsx
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
200
components/ui/dropdown-menu.tsx
Normal file
200
components/ui/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
}
|
176
components/ui/form.tsx
Normal file
176
components/ui/form.tsx
Normal file
|
@ -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<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <FormField>")
|
||||||
|
}
|
||||||
|
|
||||||
|
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<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormItem.displayName = "FormItem"
|
||||||
|
|
||||||
|
const FormLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(error && "text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormLabel.displayName = "FormLabel"
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Slot>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
|
>(({ ...props }, ref) => {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
ref={ref}
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormControl.displayName = "FormControl"
|
||||||
|
|
||||||
|
const FormDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormDescription.displayName = "FormDescription"
|
||||||
|
|
||||||
|
const FormMessage = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message) : children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-sm font-medium text-destructive", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormMessage.displayName = "FormMessage"
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
}
|
71
components/ui/grid-pattern.tsx
Normal file
71
components/ui/grid-pattern.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useId } from "react";
|
||||||
|
|
||||||
|
interface GridPatternProps {
|
||||||
|
width?: any;
|
||||||
|
height?: any;
|
||||||
|
x?: any;
|
||||||
|
y?: any;
|
||||||
|
squares?: Array<[x: number, y: number]>;
|
||||||
|
strokeDasharray?: any;
|
||||||
|
className?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GridPattern({
|
||||||
|
width = 40,
|
||||||
|
height = 40,
|
||||||
|
x = -1,
|
||||||
|
y = -1,
|
||||||
|
strokeDasharray = 0,
|
||||||
|
squares,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: GridPatternProps) {
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/35 stroke-gray-400/35",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id={id}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d={`M.5 ${height}V.5H${width}`}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
||||||
|
{squares && (
|
||||||
|
<svg x={x} y={y} className="overflow-visible">
|
||||||
|
{squares.map(([x, y]) => (
|
||||||
|
<rect
|
||||||
|
strokeWidth="0"
|
||||||
|
key={`${x}-${y}`}
|
||||||
|
width={width - 1}
|
||||||
|
height={height - 1}
|
||||||
|
x={x * width + 1}
|
||||||
|
y={y * height + 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GridPattern;
|
117
components/ui/icons.tsx
Normal file
117
components/ui/icons.tsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import { buttonVariants } from "./button";
|
||||||
|
|
||||||
|
const Iconss = () => {
|
||||||
|
const cards = {
|
||||||
|
aboutCard: {
|
||||||
|
description:
|
||||||
|
"Discover more about our services and offerings. We aim to provide the best experience for our users.",
|
||||||
|
socialLinks: {
|
||||||
|
x: "https://x.com/SVR_JS",
|
||||||
|
Mastodon: "https://mastodon.social/@svrjs",
|
||||||
|
Bluesky: "https://bsky.app/profile/svrjs.org",
|
||||||
|
Odysee: "https://odysee.com/@SVRJS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
href={cards.aboutCard.socialLinks.x}
|
||||||
|
target="_blank"
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: "ghost",
|
||||||
|
size: "sm",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className="sr-only">X icon</span>
|
||||||
|
<svg
|
||||||
|
width="25"
|
||||||
|
height="25"
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19.0039 2.34375H22.4512L14.9219 10.9473L23.7793 22.6562H16.8457L11.4111 15.5566L5.2002 22.6562H1.74805L9.7998 13.4521L1.30859 2.34375H8.41797L13.3252 8.83301L19.0039 2.34375ZM17.793 20.5957H19.7021L7.37793 4.29688H5.32715L17.793 20.5957Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={cards.aboutCard.socialLinks.Mastodon}
|
||||||
|
target="_blank"
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: "ghost",
|
||||||
|
size: "sm",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Mastodon icon</span>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="25"
|
||||||
|
height="25"
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M24.163 8.74512C24.163 3.99903 20.6083 2.60743 20.6083 2.60743C17.1206 1.20606 7.85163 1.22071 4.39739 2.60743C4.39739 2.60743 0.842704 3.99903 0.842704 8.74512C0.842704 14.3945 0.4744 21.4111 6.73556 22.8613C8.99561 23.3838 10.9376 23.4961 12.5001 23.418C15.3349 23.2813 16.9253 22.5342 16.9253 22.5342L16.8304 20.7324C16.8304 20.7324 14.8048 21.2891 12.528 21.2256C10.2735 21.1572 7.89627 21.0107 7.52797 18.5889C7.49366 18.3639 7.47688 18.1371 7.47775 17.9102C12.2545 18.9307 16.3338 18.3545 17.4554 18.2373C20.586 17.9102 23.3148 16.2207 23.6608 14.6777C24.2077 12.2461 24.163 8.74512 24.163 8.74512ZM19.9722 14.8584H17.3717V9.28223C17.3717 6.85548 13.8003 6.7627 13.8003 9.61915V12.6709H11.2166V9.61915C11.2166 6.7627 7.64516 6.85548 7.64516 9.28223V14.8584H5.03355C5.03355 8.89649 4.74337 7.63673 6.06034 6.31348C7.50565 4.90235 10.5135 4.80958 11.8527 6.61134L12.5001 7.56348L13.1474 6.61134C14.4923 4.79981 17.5057 4.91212 18.9398 6.31348C20.2623 7.64649 19.9666 8.90137 19.9666 14.8584H19.9722Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={cards.aboutCard.socialLinks.Bluesky}
|
||||||
|
target="_blank"
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: "ghost",
|
||||||
|
size: "sm",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Bluesky icon</span>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="25"
|
||||||
|
height="25"
|
||||||
|
viewBox="0 0 576 512"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M407.8 294.7C404.5 294.3 401.1 293.9 397.8 293.4C401.2 293.8 404.5 294.3 407.8 294.7ZM288 227.1C261.9 176.4 190.9 81.9 124.9 35.3C61.6 -9.40003 37.5 -1.70003 21.6 5.49997C3.3 13.8 0 41.9 0 58.4C0 74.9 9.1 194 15 213.9C34.5 279.6 104.1 301.8 168.2 294.6C171.5 294.1 174.8 293.7 178.2 293.2C174.9 293.7 171.6 294.2 168.2 294.6C74.3 308.6 -9.1 342.8 100.3 464.5C220.6 589.1 265.1 437.8 288 361.1C310.9 437.8 337.2 583.6 473.6 464.5C576 361.1 501.7 308.5 407.8 294.6C404.5 294.2 401.1 293.8 397.8 293.3C401.2 293.7 404.5 294.2 407.8 294.6C471.9 301.7 541.4 279.5 561 213.9C566.9 194 576 75 576 58.4C576 41.8 572.7 13.7 554.4 5.49997C538.6 -1.60003 514.4 -9.40003 451.2 35.3C385.1 81.9 314.1 176.4 288 227.1Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={cards.aboutCard.socialLinks.Odysee}
|
||||||
|
target="_blank"
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: "ghost",
|
||||||
|
size: "sm",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Odysee icon</span>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="25"
|
||||||
|
height="25"
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19.8584 22.6074C17.793 24.1113 15.249 25 12.5 25C7.0752 25 2.45605 21.543 0.727539 16.709C0.844727 16.792 1.01562 16.8848 1.11328 16.9238C1.90918 17.2852 3.07129 16.6602 4.18457 15.5225C4.52148 15.1855 4.8877 14.917 5.29785 14.6777C6.19141 14.0967 7.13379 13.6621 8.14941 13.3496C8.14941 13.3496 9.23828 15.0195 10.2539 17.002C11.2695 18.9844 9.16504 19.6387 8.92578 19.6387C8.91113 19.6387 8.88672 19.6387 8.85254 19.6338C8.31543 19.6094 5.43457 19.4873 6.11816 22.1289C6.8457 24.9316 10.8789 23.916 12.9346 22.5635C14.9902 21.2109 14.4824 16.7871 14.4824 16.7871C16.4893 16.4746 17.1191 18.5986 17.3096 19.6875C17.3486 19.9121 17.3633 20.1709 17.3779 20.4443C17.4316 21.4795 17.4902 22.6709 19.1016 22.71C19.3604 22.71 19.6191 22.6709 19.8584 22.6123V22.6074ZM15.2051 21.4502C15.1074 21.4258 15.0342 21.3281 15.0586 21.2061C15.1074 21.084 15.2051 21.0352 15.3027 21.0596C15.4004 21.084 15.4736 21.2061 15.4492 21.3037C15.4248 21.4014 15.3271 21.4746 15.2051 21.4502ZM5.09766 16.7822C5.1709 16.7578 5.26855 16.8311 5.29297 16.9287C5.29297 17.0264 5.24414 17.124 5.14648 17.124C5.07324 17.1484 4.97559 17.0752 4.95117 16.9775C4.92676 16.9043 5 16.8066 5.09766 16.7822ZM22.0605 20.5566C23.8916 18.3789 25 15.5664 25 12.5C25 9.2041 23.7256 6.20605 21.6406 3.96973C21.6357 5.11719 21.3428 6.32324 20.8203 7.34863C20.2393 8.33984 18.4277 10.2246 17.4121 11.1914C17.3779 11.2061 17.3584 11.2354 17.3389 11.2598C17.3291 11.2695 17.3242 11.2793 17.3145 11.2891C17.0703 11.626 17.1191 12.1094 17.4609 12.3535C18.501 13.125 20.2148 14.5801 20.3613 15.9082C20.5322 17.6123 21.7236 19.5996 22.0313 20.1172C22.0703 20.1807 22.0947 20.2197 22.0996 20.2344C22.0996 20.3418 22.0801 20.4443 22.0605 20.5518V20.5566ZM19.0771 12.2559C19.0527 12.4023 19.126 12.5439 19.2725 12.5684C19.4189 12.5928 19.5605 12.5195 19.585 12.373C19.6094 12.2266 19.5361 12.085 19.3896 12.0605C19.2432 12.0117 19.1016 12.1094 19.0771 12.2559ZM22.0752 9.28223L21.5186 9.5459L21.3721 10.1758L21.1084 9.61914L20.4785 9.47266L21.0352 9.20898L21.1816 8.5791L21.4453 9.13574L22.0752 9.28223ZM19.3115 2.0166C18.5205 2.41699 18.2324 3.61816 17.8955 5.01465C17.8809 5.08301 17.8613 5.15137 17.8467 5.21973C17.3828 7.09961 16.3525 7.05566 15.8105 7.03613C15.7568 7.03613 15.7129 7.03125 15.6689 7.03125C15.4199 7.03125 15.376 6.83594 15.2344 6.19629C15.1074 5.60547 14.8975 4.63379 14.3604 3.09082C13.252 -0.12207 10.3223 0.678711 8.10059 2.00684C5.40039 3.62305 6.4209 6.98242 7.13379 9.30176C7.16797 9.40918 7.20215 9.5166 7.23145 9.62402C7.03613 9.81934 6.55762 9.99023 5.96191 10.2051C5.37109 10.415 4.66309 10.6689 3.99414 11.0303C2.33887 11.9092 0.561523 13.4229 0.0976562 14.0869C0.0341797 13.5645 0 13.0371 0 12.5C0 5.5957 5.5957 0 12.5 0C15.0098 0 17.3535 0.742187 19.3115 2.0166ZM2.87598 9.25781C2.80273 9.16016 2.65625 9.11133 2.56348 9.18457C2.4707 9.25781 2.41699 9.4043 2.49023 9.49707C2.56348 9.58984 2.70996 9.64355 2.80273 9.57031C2.9248 9.49707 2.94922 9.35059 2.87598 9.25781ZM15.9814 3.16895C16.0791 3.0957 16.2256 3.14453 16.2939 3.24219C16.3672 3.36426 16.3428 3.50586 16.2207 3.55469C16.123 3.62793 15.9766 3.5791 15.9082 3.48145C15.8398 3.38379 15.8838 3.2373 15.9814 3.16895ZM4.64355 5.12695C4.61914 5.2002 4.66797 5.27344 4.74121 5.27344C4.81445 5.29785 4.8877 5.24902 4.8877 5.17578C4.91211 5.10254 4.86328 5.0293 4.79004 5.0293C4.7168 5.0293 4.64355 5.05371 4.64355 5.12695ZM8.7793 5.10254C8.6084 2.99805 10.5908 2.46582 10.5908 2.46582C12.7441 1.71387 13.3252 2.75391 13.833 4.30176C14.3408 5.84961 13.9795 6.38184 11.9482 7.17773C9.91699 7.97363 8.92578 6.95801 8.7793 5.09766V5.10254ZM13.1543 5.83008H13.2031C13.3252 5.83008 13.4473 5.73242 13.4473 5.58594C13.5449 5.24902 13.4961 4.88281 13.3496 4.57031C13.2764 4.47266 13.1543 4.39941 13.0371 4.44824C12.8906 4.49707 12.8174 4.64355 12.8662 4.78516C12.9639 5.00488 13.0127 5.26855 12.9395 5.5127C12.915 5.65918 13.0127 5.80078 13.1592 5.8252L13.1543 5.83008ZM12.6709 3.79883C12.5732 3.79883 12.4756 3.75 12.4268 3.65234C12.3779 3.55469 12.3291 3.48145 12.2803 3.4082C12.1826 3.31055 12.1826 3.14453 12.2803 3.04688C12.3779 2.94922 12.5439 2.94922 12.6416 3.04688C12.7393 3.16895 12.8125 3.29102 12.8857 3.4082C12.959 3.52539 12.9102 3.69629 12.7637 3.76953C12.7344 3.76953 12.7148 3.7793 12.7002 3.78418C12.6904 3.78906 12.6807 3.79395 12.6709 3.79395V3.79883Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Iconss;
|
25
components/ui/input.tsx
Normal file
25
components/ui/input.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
26
components/ui/label.tsx
Normal file
26
components/ui/label.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
128
components/ui/navigation-menu.tsx
Normal file
128
components/ui/navigation-menu.tsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const NavigationMenu = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<NavigationMenuViewport />
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
))
|
||||||
|
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const NavigationMenuList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||||
|
|
||||||
|
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
||||||
|
)
|
||||||
|
|
||||||
|
const NavigationMenuTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDown
|
||||||
|
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const NavigationMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||||
|
|
||||||
|
const NavigationMenuViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
NavigationMenuViewport.displayName =
|
||||||
|
NavigationMenuPrimitive.Viewport.displayName
|
||||||
|
|
||||||
|
const NavigationMenuIndicator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
))
|
||||||
|
NavigationMenuIndicator.displayName =
|
||||||
|
NavigationMenuPrimitive.Indicator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
}
|
31
components/ui/separator.tsx
Normal file
31
components/ui/separator.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Separator }
|
140
components/ui/sheet.tsx
Normal file
140
components/ui/sheet.tsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Sheet = SheetPrimitive.Root;
|
||||||
|
|
||||||
|
const SheetTrigger = SheetPrimitive.Trigger;
|
||||||
|
|
||||||
|
const SheetClose = SheetPrimitive.Close;
|
||||||
|
|
||||||
|
const SheetPortal = SheetPrimitive.Portal;
|
||||||
|
|
||||||
|
const SheetOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const sheetVariants = cva(
|
||||||
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
side: {
|
||||||
|
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||||
|
bottom:
|
||||||
|
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||||
|
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||||
|
right:
|
||||||
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SheetContentProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
|
const SheetContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
|
SheetContentProps
|
||||||
|
>(({ side = "right", className, children, ...props }, ref) => (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(sheetVariants({ side }), className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
));
|
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const SheetHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
SheetHeader.displayName = "SheetHeader";
|
||||||
|
|
||||||
|
const SheetFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
SheetFooter.displayName = "SheetFooter";
|
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetPortal,
|
||||||
|
SheetOverlay,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue