style: style the code according to Prettier rules

This commit is contained in:
Dorian Niemiec 2024-09-07 09:12:48 +02:00
parent fe3ab6cdf1
commit 7bb89ce202
125 changed files with 5263 additions and 5263 deletions

View file

@ -3,21 +3,21 @@ import Link from "next/link";
import { FC } from "react"; import { FC } from "react";
interface CardProps { interface CardProps {
title: string; title: string;
url: string; url: string;
} }
const Card: FC<CardProps> = ({ title, url }) => { const Card: FC<CardProps> = ({ title, url }) => {
return ( return (
<div className="bg-accent border rounded-lg hover:bg-muted transition-all"> <div className="bg-accent border rounded-lg hover:bg-muted transition-all">
<Link href={url} className="group"> <Link href={url} className="group">
<div className="flex-center rounded-lg p-6"> <div className="flex-center rounded-lg p-6">
<h2 className="text-2xl font-bold mb-2">{title}</h2> <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" /> <ArrowUpRight className="w-5 h-5 mb-2 ml-2 opacity-0 group-hover:opacity-100 transition-all duration-300" />
</div> </div>
</Link> </Link>
</div> </div>
); );
}; };
export default Card; export default Card;

View file

@ -9,49 +9,49 @@ import { Menu } from "lucide-react";
import Logo from "@/components/shared/Logo"; import Logo from "@/components/shared/Logo";
const MobileNav = () => { const MobileNav = () => {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<header className="header"> <header className="header">
<Link href="/" className="flex items-center gap-2 md:py-2"> <Link href="/" className="flex items-center gap-2 md:py-2">
<Logo width={120} height={40} /> <Logo width={120} height={40} />
</Link> </Link>
<nav className="flex gap-2"> <nav className="flex gap-2">
<Sheet> <Sheet>
<SheetTrigger> <SheetTrigger>
<Menu className="w-5 h-5" /> <Menu className="w-5 h-5" />
</SheetTrigger> </SheetTrigger>
<SheetContent className="sheet-content sm:w-64"> <SheetContent className="sheet-content sm:w-64">
<> <>
<Logo width={155} height={53} /> <Logo width={155} height={53} />
<ul className="header-nav_elements"> <ul className="header-nav_elements">
{AdminLinks.slice(0, 6).map((link) => { {AdminLinks.slice(0, 6).map((link) => {
const isActive = link.url === pathname; const isActive = link.url === pathname;
return ( return (
<li <li
key={link.url} key={link.url}
className={`${ className={`${
isActive && "gradient-text" isActive && "gradient-text"
} p-18 flex whitespace-nowrap text-dark-700`} } p-18 flex whitespace-nowrap text-dark-700`}
> >
<Link <Link
className="sidebar-link cursor-pointer" className="sidebar-link cursor-pointer"
href={link.url} href={link.url}
> >
<link.icon /> <link.icon />
{link.name} {link.name}
</Link> </Link>
</li> </li>
); );
})} })}
</ul> </ul>
</> </>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
</nav> </nav>
</header> </header>
); );
}; };
export default MobileNav; export default MobileNav;

View file

@ -6,60 +6,60 @@ import { AdminLinks } from "@/constants";
import Logo from "@/components/shared/Logo"; import Logo from "@/components/shared/Logo";
const Sidebar = () => { const Sidebar = () => {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<> <>
<aside className="sidebar"> <aside className="sidebar">
<div className="flex size-full flex-col gap-4"> <div className="flex size-full flex-col gap-4">
<Link href="/" className="sidebar-logo"> <Link href="/" className="sidebar-logo">
<Logo width={155} height={53} /> <Logo width={155} height={53} />
</Link> </Link>
<nav className="sidebar-nav"> <nav className="sidebar-nav">
<ul className="sidebar-nav_elements"> <ul className="sidebar-nav_elements">
{AdminLinks.slice(0, 6).map((link) => { {AdminLinks.slice(0, 6).map((link) => {
const isActive = link.url === pathname; const isActive = link.url === pathname;
return ( return (
<li <li
key={link.url} key={link.url}
className={`sidebar-nav_element group ${ className={`sidebar-nav_element group ${
isActive ? "bg-white/5" : "text-muted-foreground" isActive ? "bg-white/5" : "text-muted-foreground"
}`} }`}
> >
<Link className="sidebar-link" href={link.url}> <Link className="sidebar-link" href={link.url}>
<link.icon /> <link.icon />
{link.name} {link.name}
</Link> </Link>
</li> </li>
); );
})} })}
</ul> </ul>
<ul className="sidebar-nav_elements"> <ul className="sidebar-nav_elements">
{AdminLinks.slice(6).map((link) => { {AdminLinks.slice(6).map((link) => {
const isActive = link.url === pathname; const isActive = link.url === pathname;
return ( return (
<li <li
key={link.url} key={link.url}
className={`sidebar-nav_element group ${ className={`sidebar-nav_element group ${
isActive ? "bg-purple-gradient" : "text-muted-foreground" isActive ? "bg-purple-gradient" : "text-muted-foreground"
}`} }`}
> >
<Link className="sidebar-link" href={link.url}> <Link className="sidebar-link" href={link.url}>
<link.icon /> <link.icon />
{link.name} {link.name}
</Link> </Link>
</li> </li>
); );
})} })}
</ul> </ul>
</nav> </nav>
</div> </div>
</aside> </aside>
</> </>
); );
}; };
export default Sidebar; export default Sidebar;

View file

@ -1,9 +1,9 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Admin // Changelogs", title: "Admin // Changelogs"
}; };
export default function logPages({ children }: { children: React.ReactNode }) { export default function logPages({ children }: { children: React.ReactNode }) {
return <>{children}</>; return <>{children}</>;
} }

View file

@ -5,21 +5,21 @@ import { useForm, SubmitHandler, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@/components/ui/form"; } from "@/components/ui/form";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCaption,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { logsSchema } from "@/lib/validations/validation"; import { logsSchema } from "@/lib/validations/validation";
@ -27,206 +27,206 @@ import { z } from "zod";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
interface LogEntry { interface LogEntry {
_id: string; _id: string;
version: string; version: string;
date: string; date: string;
bullets: { point: string }[]; bullets: { point: string }[];
} }
type LogsFormValues = z.infer<typeof logsSchema>; type LogsFormValues = z.infer<typeof logsSchema>;
const AdminLogPage = () => { const AdminLogPage = () => {
const [logs, setLogs] = useState<LogEntry[]>([]); const [logs, setLogs] = useState<LogEntry[]>([]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const form = useForm<LogsFormValues>({ const form = useForm<LogsFormValues>({
resolver: zodResolver(logsSchema), resolver: zodResolver(logsSchema),
defaultValues: { defaultValues: {
version: "", version: "",
date: "", date: "",
bullets: [{ point: "" }], bullets: [{ point: "" }]
}, }
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control: form.control, control: form.control,
name: "bullets", name: "bullets"
}); });
const fetchLogs = async () => { const fetchLogs = async () => {
try { try {
const response = await fetch("/api/logs", { method: "GET" }); const response = await fetch("/api/logs", { method: "GET" });
if (response.ok) { if (response.ok) {
const data: LogEntry[] = await response.json(); const data: LogEntry[] = await response.json();
setLogs(data); setLogs(data);
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to fetch logs"); setError(error.message || "Failed to fetch logs");
} }
}; };
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => { const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
setLoading(true); setLoading(true);
const response = await fetch("/api/uploadlogs", { const response = await fetch("/api/uploadlogs", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), body: JSON.stringify(data)
}); });
if (response.ok) { if (response.ok) {
form.reset(); form.reset();
fetchLogs(); fetchLogs();
setLoading(false); setLoading(false);
toast({ description: "Logs successfully added" }); toast({ description: "Logs successfully added" });
} else { } else {
setLoading(false); setLoading(false);
toast({ description: "Upload Failed", variant: "destructive" }); toast({ description: "Upload Failed", variant: "destructive" });
} }
}; };
const deleteLog = async (id: string) => { const deleteLog = async (id: string) => {
try { try {
const response = await fetch(`/api/delete/logs/${id}`, { const response = await fetch(`/api/delete/logs/${id}`, {
method: "DELETE", method: "DELETE"
}); });
if (response.ok) { if (response.ok) {
fetchLogs(); fetchLogs();
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to delete log"); setError(error.message || "Failed to delete log");
} }
}; };
useEffect(() => { useEffect(() => {
fetchLogs(); fetchLogs();
const interval = setInterval(() => { const interval = setInterval(() => {
fetchLogs(); fetchLogs();
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
return ( return (
<section id="logs-page" className="wrapper container"> <section id="logs-page" className="wrapper container">
<h1 className="text-3xl font-bold py-6">Server Logs Form</h1> <h1 className="text-3xl font-bold py-6">Server Logs Form</h1>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="version" name="version"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Version Name</FormLabel> <FormLabel>Version Name</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="date" name="date"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Date</FormLabel> <FormLabel>Date</FormLabel>
<FormControl> <FormControl>
<Input {...field} placeholder="Released on 24 Nov 2024" /> <Input {...field} placeholder="Released on 24 Nov 2024" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{fields.map((field, index) => ( {fields.map((field, index) => (
<FormField <FormField
key={field.id} key={field.id}
control={form.control} control={form.control}
name={`bullets.${index}.point`} name={`bullets.${index}.point`}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Key Point {index + 1}</FormLabel> <FormLabel>Key Point {index + 1}</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<Button <Button
type="button" type="button"
className="mt-2" className="mt-2"
variant={"secondary"} variant={"secondary"}
onClick={() => remove(index)} onClick={() => remove(index)}
> >
Remove Remove
</Button> </Button>
</FormItem> </FormItem>
)} )}
/> />
))} ))}
<Button <Button
type="button" type="button"
className="mb-4" className="mb-4"
size={"icon"} size={"icon"}
variant={"outline"} variant={"outline"}
onClick={() => append({ point: "" })} onClick={() => append({ point: "" })}
> >
+ +
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="w-full text-lg rounded-full" className="w-full text-lg rounded-full"
disabled={loading} disabled={loading}
size={"lg"} size={"lg"}
> >
Submit Submit
</Button> </Button>
</form> </form>
</Form> </Form>
{/* Section to list and delete logs */} {/* Section to list and delete logs */}
<section id="logs-list" className="py-16 md:py-24"> <section id="logs-list" className="py-16 md:py-24">
<h2 className="text-3xl md:text-4xl font-bold">Existing Logs</h2> <h2 className="text-3xl md:text-4xl font-bold">Existing Logs</h2>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
<Table className="w-full mt-4 border-muted"> <Table className="w-full mt-4 border-muted">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="border-b px-4 py-2">Version</TableHead> <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">Date</TableHead>
<TableHead className="border-b px-4 py-2">Actions</TableHead> <TableHead className="border-b px-4 py-2">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{logs {logs
.slice() .slice()
.reverse() .reverse()
.map((log) => ( .map((log) => (
<TableRow key={log._id}> <TableRow key={log._id}>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{log.version} {log.version}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{log.date} {log.date}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
<Button <Button
variant={"destructive"} variant={"destructive"}
onClick={() => deleteLog(log._id)} onClick={() => deleteLog(log._id)}
> >
Delete Delete
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</section> </section>
</section> </section>
); );
}; };
export default AdminLogPage; export default AdminLogPage;

View file

@ -1,9 +1,9 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Admin // Downloads", title: "Admin // Downloads"
}; };
export default function logPages({ children }: { children: React.ReactNode }) { export default function logPages({ children }: { children: React.ReactNode }) {
return <>{children}</>; return <>{children}</>;
} }

View file

@ -6,246 +6,246 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { UploadButton } from "@/lib/uploadthing"; import { UploadButton } from "@/lib/uploadthing";
import { downloadSchema } from "@/lib/validations/validation"; import { downloadSchema } from "@/lib/validations/validation";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
interface DownloadEntry { interface DownloadEntry {
_id: string; _id: string;
fileName: string; fileName: string;
version: string; version: string;
downloadLink: string; downloadLink: string;
fileSize: string; fileSize: string;
} }
const DownloadsPage = () => { const DownloadsPage = () => {
const { toast } = useToast(); const { toast } = useToast();
const [downloads, setDownloads] = useState<DownloadEntry[]>([]); const [downloads, setDownloads] = useState<DownloadEntry[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const form = useForm<z.infer<typeof downloadSchema>>({ const form = useForm<z.infer<typeof downloadSchema>>({
resolver: zodResolver(downloadSchema), resolver: zodResolver(downloadSchema),
defaultValues: { defaultValues: {
fileName: "", fileName: "",
version: "", version: "",
downloadLink: "", downloadLink: "",
fileSize: "", fileSize: ""
}, }
}); });
const fetchDownloads = async () => { const fetchDownloads = async () => {
try { try {
const response = await fetch("/api/downloads", { const response = await fetch("/api/downloads", {
method: "GET", method: "GET"
}); });
if (response.ok) { if (response.ok) {
const data: DownloadEntry[] = await response.json(); const data: DownloadEntry[] = await response.json();
setDownloads(data); setDownloads(data);
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to fetch downloads"); setError(error.message || "Failed to fetch downloads");
} }
}; };
useEffect(() => { useEffect(() => {
fetchDownloads(); fetchDownloads();
const interval = setInterval(() => { const interval = setInterval(() => {
fetchDownloads(); fetchDownloads();
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const onSubmit: SubmitHandler<z.infer<typeof downloadSchema>> = async ( const onSubmit: SubmitHandler<z.infer<typeof downloadSchema>> = async (
data data
) => { ) => {
setLoading(true); setLoading(true);
const response = await fetch("/api/upload", { const response = await fetch("/api/upload", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, },
body: JSON.stringify(data), body: JSON.stringify(data)
}); });
if (response.ok) { if (response.ok) {
form.reset(); form.reset();
fetchDownloads(); fetchDownloads();
setLoading(false); setLoading(false);
toast({ description: "Download Successfully Updated" }); toast({ description: "Download Successfully Updated" });
} else { } else {
console.error("Upload failed"); console.error("Upload failed");
setLoading(false); setLoading(false);
toast({ description: "Uploading Failed", variant: "destructive" }); toast({ description: "Uploading Failed", variant: "destructive" });
} }
}; };
const deleteDownload = async (id: string) => { const deleteDownload = async (id: string) => {
try { try {
const response = await fetch(`/api/delete/downloads/${id}`, { const response = await fetch(`/api/delete/downloads/${id}`, {
method: "DELETE", method: "DELETE"
}); });
if (response.ok) { if (response.ok) {
fetchDownloads(); fetchDownloads();
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to delete download"); setError(error.message || "Failed to delete download");
} }
}; };
return ( return (
<section id="downloads-page" className="wrapper container"> <section id="downloads-page" className="wrapper container">
<h1 className="text-3xl font-bold py-6">Downloads Form</h1> <h1 className="text-3xl font-bold py-6">Downloads Form</h1>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="fileName" name="fileName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>File Name</FormLabel> <FormLabel>File Name</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="version" name="version"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Version</FormLabel> <FormLabel>Version</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="downloadLink" name="downloadLink"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Download Link</FormLabel> <FormLabel>Download Link</FormLabel>
<UploadButton <UploadButton
endpoint="imageUploader" endpoint="imageUploader"
onClientUploadComplete={(res) => { onClientUploadComplete={(res) => {
field.onChange(res[0].url); field.onChange(res[0].url);
}} }}
onUploadError={(error: Error) => { onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`); alert(`ERROR! ${error.message}`);
}} }}
/> />
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="fileSize" name="fileSize"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>File Size</FormLabel> <FormLabel>File Size</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<Button <Button
type="submit" type="submit"
className="w-full text-lg rounded-full" className="w-full text-lg rounded-full"
size={"lg"} size={"lg"}
disabled={loading} disabled={loading}
> >
Submit Submit
</Button> </Button>
</form> </form>
</Form> </Form>
{/* Section to list and delete downloads */} {/* Section to list and delete downloads */}
<section id="downloads-list" className="py-16 md:py-24"> <section id="downloads-list" className="py-16 md:py-24">
<h2 className="text-3xl md:text-4xl font-bold">Existing Downloads</h2> <h2 className="text-3xl md:text-4xl font-bold">Existing Downloads</h2>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
<Table className="w-full mt-4 border-muted"> <Table className="w-full mt-4 border-muted">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="border-b px-4 py-2">File Name</TableHead> <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">Version</TableHead>
<TableHead className="border-b px-4 py-2"> <TableHead className="border-b px-4 py-2">
Download Link Download Link
</TableHead> </TableHead>
<TableHead className="border-b px-4 py-2">File Size</TableHead> <TableHead className="border-b px-4 py-2">File Size</TableHead>
<TableHead className="border-b px-4 py-2">Actions</TableHead> <TableHead className="border-b px-4 py-2">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{downloads {downloads
.slice() .slice()
.reverse() .reverse()
.map((download) => ( .map((download) => (
<TableRow key={download._id}> <TableRow key={download._id}>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{download.fileName} {download.fileName}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{download.version} {download.version}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
<a <a
href={download.downloadLink} href={download.downloadLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{download.downloadLink} {download.downloadLink}
</a> </a>
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{download.fileSize} {download.fileSize}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
<Button <Button
variant={"destructive"} variant={"destructive"}
onClick={() => deleteDownload(download._id)} onClick={() => deleteDownload(download._id)}
> >
Delete Delete
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</section> </section>
</section> </section>
); );
}; };
export default DownloadsPage; export default DownloadsPage;

View file

@ -4,130 +4,130 @@ import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
import { import {
Pagination, Pagination,
PaginationContent, PaginationContent,
PaginationItem, PaginationItem,
PaginationLink, PaginationLink,
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import Link from "next/link"; import Link from "next/link";
interface Subscriber { interface Subscriber {
email: string; email: string;
subscribedAt: string; subscribedAt: string;
} }
const EmailPage = () => { const EmailPage = () => {
const [subscribers, setSubscribers] = useState<Subscriber[]>([]); const [subscribers, setSubscribers] = useState<Subscriber[]>([]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
const { toast } = useToast(); const { toast } = useToast();
useEffect(() => { useEffect(() => {
// Function to fetch subscribers data // Function to fetch subscribers data
const fetchSubscribers = async () => { const fetchSubscribers = async () => {
try { try {
const res = await fetch( const res = await fetch(
`/api/newsletter/subscriber?page=${currentPage}` `/api/newsletter/subscriber?page=${currentPage}`
); );
const data = await res.json(); const data = await res.json();
setSubscribers(data.subscribers); setSubscribers(data.subscribers);
setTotalPages(data.totalPages); setTotalPages(data.totalPages);
} catch (error) { } catch (error) {
toast({ toast({
title: "Error fetching subscribers", title: "Error fetching subscribers",
description: `${error}`, description: `${error}`
}); });
} }
}; };
// Fetch data initially // Fetch data initially
fetchSubscribers(); fetchSubscribers();
// Set up interval to fetch data every 10 seconds // Set up interval to fetch data every 10 seconds
const intervalId = setInterval(fetchSubscribers, 10000); const intervalId = setInterval(fetchSubscribers, 10000);
// Clear interval on component unmount // Clear interval on component unmount
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [currentPage, toast]); }, [currentPage, toast]);
return ( return (
<section id="downloads-page" className="wrapper container"> <section id="downloads-page" className="wrapper container">
<h1 className="text-3xl md:text-4xl font-bold py-6">Newsletter Emails</h1> <h1 className="text-3xl md:text-4xl font-bold py-6">Newsletter Emails</h1>
<Link href="/email-editor"> <Link href="/email-editor">
<Button>Create a new email</Button> <Button>Create a new email</Button>
</Link> </Link>
<section id="downloads-list" className="py-8"> <section id="downloads-list" className="py-8">
<h2 className="text-2xl font-semibold">Newsletter Subscribers</h2> <h2 className="text-2xl font-semibold">Newsletter Subscribers</h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Total subscribers: {subscribers.length} Total subscribers: {subscribers.length}
</p> </p>
<Table className="w-full mt-4 border-muted"> <Table className="w-full mt-4 border-muted">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="border-b px-4 py-2">Email</TableHead> <TableHead className="border-b px-4 py-2">Email</TableHead>
<TableHead className="border-b px-4 py-2"> <TableHead className="border-b px-4 py-2">
Subscribed At Subscribed At
</TableHead> </TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{subscribers.map((subscriber, idx) => ( {subscribers.map((subscriber, idx) => (
<TableRow key={idx}> <TableRow key={idx}>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{subscriber.email} {subscriber.email}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{new Date(subscriber.subscribedAt).toLocaleDateString()} {new Date(subscriber.subscribedAt).toLocaleDateString()}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
<div className="flex-center mt-12"> <div className="flex-center mt-12">
{totalPages > 1 && ( {totalPages > 1 && (
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
{currentPage > 1 && ( {currentPage > 1 && (
<PaginationPrevious <PaginationPrevious
onClick={() => setCurrentPage(currentPage - 1)} onClick={() => setCurrentPage(currentPage - 1)}
/> />
)} )}
</PaginationItem> </PaginationItem>
{Array.from({ length: totalPages }).map((_, i) => ( {Array.from({ length: totalPages }).map((_, i) => (
<PaginationItem key={i}> <PaginationItem key={i}>
<PaginationLink <PaginationLink
isActive={currentPage === i + 1} isActive={currentPage === i + 1}
onClick={() => setCurrentPage(i + 1)} onClick={() => setCurrentPage(i + 1)}
> >
{i + 1} {i + 1}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
))} ))}
<PaginationItem> <PaginationItem>
{currentPage < totalPages && ( {currentPage < totalPages && (
<PaginationNext <PaginationNext
onClick={() => setCurrentPage(currentPage + 1)} onClick={() => setCurrentPage(currentPage + 1)}
/> />
)} )}
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination> </Pagination>
)} )}
</div> </div>
</section> </section>
</section> </section>
); );
}; };
export default EmailPage; export default EmailPage;

View file

@ -2,15 +2,15 @@ import MobileNav from "../_components/Mobilenav";
import Sidebar from "../_components/Sidebar"; import Sidebar from "../_components/Sidebar";
export default function PageLayout({ export default function PageLayout({
children, children
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<main className="flex flex-col min-h-screen root"> <main className="flex flex-col min-h-screen root">
<Sidebar /> <Sidebar />
<MobileNav /> <MobileNav />
<div className="root-container lg:px-24">{children}</div> <div className="root-container lg:px-24">{children}</div>
</main> </main>
); );
} }

View file

@ -1,9 +1,9 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Admin // Mods", title: "Admin // Mods"
}; };
export default function logPages({ children }: { children: React.ReactNode }) { export default function logPages({ children }: { children: React.ReactNode }) {
return <>{children}</>; return <>{children}</>;
} }

View file

@ -6,401 +6,401 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { UploadButton } from "@/lib/uploadthing"; import { UploadButton } from "@/lib/uploadthing";
import { modsSchema } from "@/lib/validations/validation"; import { modsSchema } from "@/lib/validations/validation";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
interface ModEntry { interface ModEntry {
_id: string; _id: string;
fileName: string; fileName: string;
version: string; version: string;
downloadLink: string; downloadLink: string;
fileSize: string; fileSize: string;
} }
const SvrjsModsAdminPage = () => { const SvrjsModsAdminPage = () => {
const { toast } = useToast(); const { toast } = useToast();
const [mods, setMods] = useState<ModEntry[]>([]); const [mods, setMods] = useState<ModEntry[]>([]);
const [editMod, setEditMod] = useState<ModEntry | null>(null); const [editMod, setEditMod] = useState<ModEntry | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const mainForm = useForm<z.infer<typeof modsSchema>>({ const mainForm = useForm<z.infer<typeof modsSchema>>({
resolver: zodResolver(modsSchema), resolver: zodResolver(modsSchema),
defaultValues: { defaultValues: {
fileName: "", fileName: "",
version: "", version: "",
downloadLink: "", downloadLink: "",
fileSize: "", fileSize: ""
}, }
}); });
const dialogForm = useForm<z.infer<typeof modsSchema>>({ const dialogForm = useForm<z.infer<typeof modsSchema>>({
resolver: zodResolver(modsSchema), resolver: zodResolver(modsSchema),
defaultValues: { defaultValues: {
fileName: "", fileName: "",
version: "", version: "",
downloadLink: "", downloadLink: "",
fileSize: "", fileSize: ""
}, }
}); });
useEffect(() => { useEffect(() => {
fetchMods(); fetchMods();
const interval = setInterval(() => { const interval = setInterval(() => {
fetchMods(); fetchMods();
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
useEffect(() => { useEffect(() => {
if (editMod) { if (editMod) {
dialogForm.reset({ dialogForm.reset({
fileName: editMod.fileName, fileName: editMod.fileName,
version: editMod.version, version: editMod.version,
downloadLink: editMod.downloadLink, downloadLink: editMod.downloadLink,
fileSize: editMod.fileSize, fileSize: editMod.fileSize
}); });
setDialogOpen(true); // Open dialog when a mod is being edited setDialogOpen(true); // Open dialog when a mod is being edited
} }
}, [editMod]); }, [editMod]);
const fetchMods = async () => { const fetchMods = async () => {
try { try {
const response = await fetch("/api/mods", { const response = await fetch("/api/mods", {
method: "GET", method: "GET"
}); });
if (response.ok) { if (response.ok) {
const data: ModEntry[] = await response.json(); const data: ModEntry[] = await response.json();
setMods(data); setMods(data);
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to fetch mods"); setError(error.message || "Failed to fetch mods");
} }
}; };
const onSubmit: SubmitHandler<z.infer<typeof modsSchema>> = async (data) => { const onSubmit: SubmitHandler<z.infer<typeof modsSchema>> = async (data) => {
setLoading(true); setLoading(true);
try { try {
const response = editMod const response = editMod
? await fetch(`/api/update/mods/${editMod._id}`, { ? await fetch(`/api/update/mods/${editMod._id}`, {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, },
body: JSON.stringify(data), body: JSON.stringify(data)
}) })
: await fetch("/api/uploadmods", { : await fetch("/api/uploadmods", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, },
body: JSON.stringify(data), body: JSON.stringify(data)
}); });
if (response.ok) { if (response.ok) {
mainForm.reset(); mainForm.reset();
dialogForm.reset(); dialogForm.reset();
fetchMods(); fetchMods();
setLoading(false); setLoading(false);
setEditMod(null); setEditMod(null);
setDialogOpen(false); // Close dialog on successful submission setDialogOpen(false); // Close dialog on successful submission
toast({ toast({
description: "Successfully Saved Changes", description: "Successfully Saved Changes"
}); });
} else { } else {
console.error("Save failed"); console.error("Save failed");
setLoading(false); setLoading(false);
toast({ toast({
description: "Save failed", description: "Save failed",
variant: "destructive", variant: "destructive"
}); });
} }
} catch (error) { } catch (error) {
console.error("Save failed", error); console.error("Save failed", error);
setLoading(false); setLoading(false);
toast({ toast({
description: "Save failed", description: "Save failed",
variant: "destructive", variant: "destructive"
}); });
} }
}; };
const deleteMod = async (id: string) => { const deleteMod = async (id: string) => {
try { try {
const response = await fetch(`/api/delete/mods/${id}`, { const response = await fetch(`/api/delete/mods/${id}`, {
method: "DELETE", method: "DELETE"
}); });
if (response.ok) { if (response.ok) {
fetchMods(); fetchMods();
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to delete mod"); setError(error.message || "Failed to delete mod");
} }
}; };
return ( return (
<section id="mods-page" className="wrapper container"> <section id="mods-page" className="wrapper container">
<h1 className="text-3xl font-bold py-6">Mods Form</h1> <h1 className="text-3xl font-bold py-6">Mods Form</h1>
<Form {...mainForm}> <Form {...mainForm}>
<form onSubmit={mainForm.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={mainForm.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={mainForm.control} control={mainForm.control}
name="fileName" name="fileName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>File Name</FormLabel> <FormLabel>File Name</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={mainForm.control} control={mainForm.control}
name="version" name="version"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Version</FormLabel> <FormLabel>Version</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={mainForm.control} control={mainForm.control}
name="downloadLink" name="downloadLink"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Download Link</FormLabel> <FormLabel>Download Link</FormLabel>
<UploadButton <UploadButton
endpoint="imageUploader" endpoint="imageUploader"
onClientUploadComplete={(res) => { onClientUploadComplete={(res) => {
field.onChange(res[0].url); field.onChange(res[0].url);
}} }}
onUploadError={(error: Error) => { onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`); alert(`ERROR! ${error.message}`);
}} }}
/> />
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={mainForm.control} control={mainForm.control}
name="fileSize" name="fileSize"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>File Size</FormLabel> <FormLabel>File Size</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<Button <Button
type="submit" type="submit"
className="w-full text-lg rounded-full" className="w-full text-lg rounded-full"
size={"lg"} size={"lg"}
disabled={loading} disabled={loading}
> >
{editMod ? "Save Changes" : "Submit"} {editMod ? "Save Changes" : "Submit"}
</Button> </Button>
</form> </form>
</Form> </Form>
{/* Section to list and delete mods */} {/* Section to list and delete mods */}
<section id="mods-list" className="py-16 md:py-24"> <section id="mods-list" className="py-16 md:py-24">
<h2 className="text-3xl md:text-4xl font-bold">Existing Mods</h2> <h2 className="text-3xl md:text-4xl font-bold">Existing Mods</h2>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
<Table className="w-full mt-4 border-muted"> <Table className="w-full mt-4 border-muted">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="border-b px-4 py-2">File Name</TableHead> <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">Version</TableHead>
<TableHead className="border-b px-4 py-2"> <TableHead className="border-b px-4 py-2">
Download Link Download Link
</TableHead> </TableHead>
<TableHead className="border-b px-4 py-2">File Size</TableHead> <TableHead className="border-b px-4 py-2">File Size</TableHead>
<TableHead className="border-b px-4 py-2">Actions</TableHead> <TableHead className="border-b px-4 py-2">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{mods {mods
.slice() .slice()
.reverse() .reverse()
.map((mod) => ( .map((mod) => (
<TableRow key={mod._id}> <TableRow key={mod._id}>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{mod.fileName} {mod.fileName}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{mod.version} {mod.version}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
<a <a
href={mod.downloadLink} href={mod.downloadLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{mod.downloadLink} {mod.downloadLink}
</a> </a>
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{mod.fileSize} {mod.fileSize}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2 gap-2 flex-center"> <TableCell className="border-b px-4 py-2 gap-2 flex-center">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger> <DialogTrigger>
<Button <Button
variant="outline" variant="outline"
onClick={() => setEditMod(mod)} onClick={() => setEditMod(mod)}
> >
Edit Edit
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>Edit Content</DialogTitle> <DialogTitle>Edit Content</DialogTitle>
<Form {...dialogForm}> <Form {...dialogForm}>
<form <form
onSubmit={dialogForm.handleSubmit(onSubmit)} onSubmit={dialogForm.handleSubmit(onSubmit)}
className="space-y-4" className="space-y-4"
> >
<FormField <FormField
control={dialogForm.control} control={dialogForm.control}
name="fileName" name="fileName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>File Name</FormLabel> <FormLabel>File Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
defaultValue={editMod?.fileName} defaultValue={editMod?.fileName}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={dialogForm.control} control={dialogForm.control}
name="version" name="version"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Version</FormLabel> <FormLabel>Version</FormLabel>
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
defaultValue={editMod?.version} defaultValue={editMod?.version}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={dialogForm.control} control={dialogForm.control}
name="downloadLink" name="downloadLink"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Download Link</FormLabel> <FormLabel>Download Link</FormLabel>
<UploadButton <UploadButton
endpoint="imageUploader" endpoint="imageUploader"
onClientUploadComplete={(res) => { onClientUploadComplete={(res) => {
field.onChange(res[0].url); field.onChange(res[0].url);
}} }}
onUploadError={(error: Error) => { onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`); alert(`ERROR! ${error.message}`);
}} }}
/> />
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
defaultValue={editMod?.downloadLink} defaultValue={editMod?.downloadLink}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={dialogForm.control} control={dialogForm.control}
name="fileSize" name="fileSize"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>File Size</FormLabel> <FormLabel>File Size</FormLabel>
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
defaultValue={editMod?.fileSize} defaultValue={editMod?.fileSize}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<Button <Button
type="submit" type="submit"
className="w-full text-lg rounded-full" className="w-full text-lg rounded-full"
size={"lg"} size={"lg"}
disabled={loading} disabled={loading}
> >
Save Changes Save Changes
</Button> </Button>
</form> </form>
</Form> </Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Button <Button
variant={"destructive"} variant={"destructive"}
onClick={() => deleteMod(mod._id)} onClick={() => deleteMod(mod._id)}
> >
Delete Delete
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</section> </section>
</section> </section>
); );
}; };
export default SvrjsModsAdminPage; export default SvrjsModsAdminPage;

View file

@ -7,79 +7,79 @@ import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), { const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), {
ssr: false, ssr: false
}); });
const EditPage = ({ params }: { params: { slug: string } }) => { const EditPage = ({ params }: { params: { slug: string } }) => {
const router = useRouter(); const router = useRouter();
const { slug } = params; const { slug } = params;
const { toast } = useToast(); const { toast } = useToast();
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [vulnerabilities, setVulnerabilities] = useState(""); const [vulnerabilities, setVulnerabilities] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (slug) { if (slug) {
fetch(`/api/mdx/pages/${slug}`) fetch(`/api/mdx/pages/${slug}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
setTitle(data.title); setTitle(data.title);
setContent(data.content); setContent(data.content);
setVulnerabilities(data.vulnerabilities || ""); setVulnerabilities(data.vulnerabilities || "");
}) })
.catch((error) => console.error("Failed to load page", error)); .catch((error) => console.error("Failed to load page", error));
} }
}, [slug]); }, [slug]);
const savePage = async () => { const savePage = async () => {
setLoading(true); setLoading(true);
const response = await fetch(`/api/mdx/pages/${slug}`, { const response = await fetch(`/api/mdx/pages/${slug}`, {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content, vulnerabilities }), body: JSON.stringify({ title, content, vulnerabilities })
}); });
if (response.ok) { if (response.ok) {
setLoading(false); setLoading(false);
toast({ description: "Page successfully updated" }); toast({ description: "Page successfully updated" });
router.push(`/admin/multi-logs/`); router.push(`/admin/multi-logs/`);
} else { } else {
setLoading(false); setLoading(false);
toast({ description: "Page Updated" }); toast({ description: "Page Updated" });
} }
}; };
const handleEditorChange = (value?: string) => { const handleEditorChange = (value?: string) => {
if (value !== undefined) { if (value !== undefined) {
setContent(value); setContent(value);
} }
}; };
return ( return (
<section id="edit-page" className="wrapper container gap-4"> <section id="edit-page" className="wrapper container gap-4">
<h1 className="text-3xl font-bold py-6">Edit Page: {slug}</h1> <h1 className="text-3xl font-bold py-6">Edit Page: {slug}</h1>
<Input <Input
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Page Title" placeholder="Page Title"
/> />
<MarkdownEditor <MarkdownEditor
value={content} value={content}
onChange={handleEditorChange} onChange={handleEditorChange}
height={560} height={560}
/> />
<h1 className="text-3xl font-bold py-6">Vulnerabilities</h1> <h1 className="text-3xl font-bold py-6">Vulnerabilities</h1>
<MarkdownEditor <MarkdownEditor
value={vulnerabilities} value={vulnerabilities}
onChange={(value) => setVulnerabilities(value || "")} onChange={(value) => setVulnerabilities(value || "")}
height={200} height={200}
/> />
<Button onClick={savePage} disabled={loading} className="mt-4"> <Button onClick={savePage} disabled={loading} className="mt-4">
Save Save
</Button> </Button>
</section> </section>
); );
}; };
export default EditPage; export default EditPage;

View file

@ -1,9 +1,9 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Admin // MultiLogs", title: "Admin // MultiLogs"
}; };
export default function logPages({ children }: { children: React.ReactNode }) { export default function logPages({ children }: { children: React.ReactNode }) {
return <>{children}</>; return <>{children}</>;
} }

View file

@ -2,157 +2,157 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogFooter, DialogFooter,
DialogTitle, DialogTitle
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
interface PageEntry { interface PageEntry {
title: string; title: string;
slug: string; slug: string;
content: string; content: string;
} }
const MultiLogs = () => { const MultiLogs = () => {
const [pages, setPages] = useState<PageEntry[]>([]); const [pages, setPages] = useState<PageEntry[]>([]);
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [pageTitle, setPageTitle] = useState(""); const [pageTitle, setPageTitle] = useState("");
useEffect(() => { useEffect(() => {
fetch("/api/mdx/pages") fetch("/api/mdx/pages")
.then((response) => response.json()) .then((response) => response.json())
.then((data) => setPages(data)) .then((data) => setPages(data))
.catch((error) => console.error("Failed to load pages", error)); .catch((error) => console.error("Failed to load pages", error));
}, []); }, []);
const createPage = async () => { const createPage = async () => {
setLoading(true); setLoading(true);
const slug = pageTitle.toLowerCase().replace(/\s+/g, "-"); const slug = pageTitle.toLowerCase().replace(/\s+/g, "-");
const response = await fetch("/api/mdx/pages", { const response = await fetch("/api/mdx/pages", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: pageTitle, slug, content: "" }), body: JSON.stringify({ title: pageTitle, slug, content: "" })
}); });
if (response.ok) { if (response.ok) {
const newPage = await response.json(); const newPage = await response.json();
setPages([...pages, newPage]); setPages([...pages, newPage]);
setPageTitle(""); setPageTitle("");
setOpen(false); setOpen(false);
router.push(`/admin/multi-logs/${slug}`); router.push(`/admin/multi-logs/${slug}`);
toast({ description: "Page created successfully" }); toast({ description: "Page created successfully" });
} else { } else {
const errorData = await response.json(); const errorData = await response.json();
console.error("Failed to create page:", errorData); console.error("Failed to create page:", errorData);
toast({ description: `Error: ${errorData.message}` }); toast({ description: `Error: ${errorData.message}` });
} }
setLoading(false); setLoading(false);
}; };
const deletePage = async (slug: string) => { const deletePage = async (slug: string) => {
setLoading(true); setLoading(true);
const response = await fetch(`/api/mdx/pages/${slug}`, { const response = await fetch(`/api/mdx/pages/${slug}`, {
method: "DELETE", method: "DELETE"
}); });
if (response.ok) { if (response.ok) {
setPages(pages.filter((page) => page.slug !== slug)); setPages(pages.filter((page) => page.slug !== slug));
toast({ description: "Page deleted successfully" }); toast({ description: "Page deleted successfully" });
} else { } else {
const errorData = await response.json(); const errorData = await response.json();
console.error("Failed to delete page:", errorData); console.error("Failed to delete page:", errorData);
toast({ description: `Error: ${errorData.message}` }); toast({ description: `Error: ${errorData.message}` });
} }
setLoading(false); setLoading(false);
}; };
return ( return (
<section id="logs-page" className="wrapper container"> <section id="logs-page" className="wrapper container">
<section id="create-page" className="py-16"> <section id="create-page" className="py-16">
<h2 className="text-3xl md:text-4xl font-bold mb-2">Create New Page</h2> <h2 className="text-3xl md:text-4xl font-bold mb-2">Create New Page</h2>
<Button variant={"secondary"} onClick={() => setOpen(true)}> <Button variant={"secondary"} onClick={() => setOpen(true)}>
Create New Page Create New Page
</Button> </Button>
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Enter Page Title</DialogTitle> <DialogTitle>Enter Page Title</DialogTitle>
</DialogHeader> </DialogHeader>
<Input <Input
value={pageTitle} value={pageTitle}
onChange={(e) => setPageTitle(e.target.value)} onChange={(e) => setPageTitle(e.target.value)}
placeholder="Page Title" placeholder="Page Title"
/> />
<DialogFooter> <DialogFooter>
<Button disabled={loading} onClick={createPage}> <Button disabled={loading} onClick={createPage}>
Continue Continue
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</section> </section>
<section id="pages-list" className="pb-16"> <section id="pages-list" className="pb-16">
<h2 className="text-3xl md:text-4xl font-bold">Existing Pages</h2> <h2 className="text-3xl md:text-4xl font-bold">Existing Pages</h2>
<p className="mb-4">Total Pages: {pages.length}</p> <p className="mb-4">Total Pages: {pages.length}</p>
<Table className="w-full mt-4 border-muted"> <Table className="w-full mt-4 border-muted">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="border-b px-4 py-2">Slug</TableHead> <TableHead className="border-b px-4 py-2">Slug</TableHead>
<TableHead className="border-b px-4 py-2">Actions</TableHead> <TableHead className="border-b px-4 py-2">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{pages.map((page) => ( {pages.map((page) => (
<TableRow key={page.slug}> <TableRow key={page.slug}>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
<a <a
href={`/changelogs/${page.slug}`} href={`/changelogs/${page.slug}`}
className="text-blue-500 underline" className="text-blue-500 underline"
> >
{page.slug} {page.slug}
</a> </a>
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
<Button <Button
variant={"outline"} variant={"outline"}
onClick={() => onClick={() =>
router.push(`/admin/multi-logs/${page.slug}`) router.push(`/admin/multi-logs/${page.slug}`)
} }
> >
Edit Edit
</Button> </Button>
<Button <Button
variant={"destructive"} variant={"destructive"}
onClick={() => deletePage(page.slug)} onClick={() => deletePage(page.slug)}
className="ml-2" className="ml-2"
disabled={loading} disabled={loading}
> >
Delete Delete
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</section> </section>
</section> </section>
); );
}; };
export default MultiLogs; export default MultiLogs;

View file

@ -3,19 +3,19 @@ import Card from "../_components/Card";
import { AdminDashboardLINKS } from "@/constants"; import { AdminDashboardLINKS } from "@/constants";
const AdminPage = () => { const AdminPage = () => {
return ( return (
<> <>
<section id="adminpage" className="wrapper container"> <section id="adminpage" className="wrapper container">
<h1 className="h2-bold py-6">Admin Page</h1> <h1 className="h2-bold py-6">Admin Page</h1>
<div className="grid lg:grid-cols-2 grid-cols-1 gap-4 "> <div className="grid lg:grid-cols-2 grid-cols-1 gap-4 ">
{AdminDashboardLINKS.map((item, idx) => ( {AdminDashboardLINKS.map((item, idx) => (
<Card key={idx} title={item.label} url={item.url} /> <Card key={idx} title={item.label} url={item.url} />
))} ))}
</div> </div>
</section> </section>
</> </>
); );
}; };
export default AdminPage; export default AdminPage;

View file

@ -1,9 +1,9 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Admin // Vulnerabilities", title: "Admin // Vulnerabilities"
}; };
export default function logPages({ children }: { children: React.ReactNode }) { export default function logPages({ children }: { children: React.ReactNode }) {
return <>{children}</>; return <>{children}</>;
} }

View file

@ -5,217 +5,217 @@ import { useForm, SubmitHandler, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@/components/ui/form"; } from "@/components/ui/form";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCaption,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { z } from "zod"; import { z } from "zod";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { vulnerabilitiesSchema } from "@/lib/validations/validation"; import { vulnerabilitiesSchema } from "@/lib/validations/validation";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger
} from "@/components/ui/select"; } from "@/components/ui/select";
interface VulnerabiltyEntry { interface VulnerabiltyEntry {
_id: string; _id: string;
version: string; version: string;
bullets: { point: string }[]; bullets: { point: string }[];
} }
type VulnerabiltiesForm = z.infer<typeof vulnerabilitiesSchema>; type VulnerabiltiesForm = z.infer<typeof vulnerabilitiesSchema>;
const AdminLogPage = () => { const AdminLogPage = () => {
const [logs, setLogs] = useState<VulnerabiltyEntry[]>([]); const [logs, setLogs] = useState<VulnerabiltyEntry[]>([]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const form = useForm<VulnerabiltiesForm>({ const form = useForm<VulnerabiltiesForm>({
resolver: zodResolver(vulnerabilitiesSchema), resolver: zodResolver(vulnerabilitiesSchema),
defaultValues: { defaultValues: {
version: "", version: "",
bullets: [{ point: "" }], bullets: [{ point: "" }]
}, }
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control: form.control, control: form.control,
name: "bullets", name: "bullets"
}); });
const fetchLogs = async () => { const fetchLogs = async () => {
try { try {
const response = await fetch("/api/vulnerabilities", { method: "GET" }); const response = await fetch("/api/vulnerabilities", { method: "GET" });
if (response.ok) { if (response.ok) {
const data: VulnerabiltyEntry[] = await response.json(); const data: VulnerabiltyEntry[] = await response.json();
setLogs(data); setLogs(data);
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to fetch logs"); setError(error.message || "Failed to fetch logs");
} }
}; };
const onSubmit: SubmitHandler<VulnerabiltiesForm> = async (data) => { const onSubmit: SubmitHandler<VulnerabiltiesForm> = async (data) => {
setLoading(true); setLoading(true);
const response = await fetch("/api/uploadvulnerabilities", { const response = await fetch("/api/uploadvulnerabilities", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), body: JSON.stringify(data)
}); });
if (response.ok) { if (response.ok) {
form.reset(); form.reset();
fetchLogs(); fetchLogs();
setLoading(false); setLoading(false);
toast({ description: "Logs successfully added" }); toast({ description: "Logs successfully added" });
} else { } else {
setLoading(false); setLoading(false);
toast({ description: "Upload Failed", variant: "destructive" }); toast({ description: "Upload Failed", variant: "destructive" });
} }
}; };
const deleteLog = async (id: string) => { const deleteLog = async (id: string) => {
try { try {
const response = await fetch(`/api/delete/vulnerability/${id}`, { const response = await fetch(`/api/delete/vulnerability/${id}`, {
method: "DELETE", method: "DELETE"
}); });
if (response.ok) { if (response.ok) {
fetchLogs(); fetchLogs();
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to delete log"); setError(error.message || "Failed to delete log");
} }
}; };
useEffect(() => { useEffect(() => {
fetchLogs(); fetchLogs();
const interval = setInterval(() => { const interval = setInterval(() => {
fetchLogs(); fetchLogs();
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
return ( return (
<section id="logs-page" className="wrapper container"> <section id="logs-page" className="wrapper container">
<h1 className="text-3xl font-bold py-6">Server Vulnerabilties Form</h1> <h1 className="text-3xl font-bold py-6">Server Vulnerabilties Form</h1>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="version" name="version"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Version Name</FormLabel> <FormLabel>Version Name</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{fields.map((field, index) => ( {fields.map((field, index) => (
<FormField <FormField
key={field.id} key={field.id}
control={form.control} control={form.control}
name={`bullets.${index}.point`} name={`bullets.${index}.point`}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Key Point {index + 1}</FormLabel> <FormLabel>Key Point {index + 1}</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<Button <Button
type="button" type="button"
className="mt-2" className="mt-2"
variant={"secondary"} variant={"secondary"}
onClick={() => remove(index)} onClick={() => remove(index)}
> >
Remove Remove
</Button> </Button>
</FormItem> </FormItem>
)} )}
/> />
))} ))}
<Button <Button
type="button" type="button"
className="mb-4" className="mb-4"
size={"icon"} size={"icon"}
variant={"outline"} variant={"outline"}
onClick={() => append({ point: "" })} onClick={() => append({ point: "" })}
> >
+ +
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="w-full text-lg rounded-full" className="w-full text-lg rounded-full"
disabled={loading} disabled={loading}
size={"lg"} size={"lg"}
> >
Submit Submit
</Button> </Button>
</form> </form>
</Form> </Form>
{/* Section to list and delete logs */} {/* Section to list and delete logs */}
<section id="logs-list" className="py-16 md:py-24"> <section id="logs-list" className="py-16 md:py-24">
<h2 className="text-3xl md:text-4xl font-bold"> <h2 className="text-3xl md:text-4xl font-bold">
Existing Vulnerabilties Existing Vulnerabilties
</h2> </h2>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
<Table className="w-full mt-4 border-muted"> <Table className="w-full mt-4 border-muted">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="border-b px-4 py-2">Version</TableHead> <TableHead className="border-b px-4 py-2">Version</TableHead>
<TableHead className="border-b px-4 py-2">Actions</TableHead> <TableHead className="border-b px-4 py-2">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{logs {logs
.slice() .slice()
.reverse() .reverse()
.map((log) => ( .map((log) => (
<TableRow key={log._id}> <TableRow key={log._id}>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
{log.version} {log.version}
</TableCell> </TableCell>
<TableCell className="border-b px-4 py-2"> <TableCell className="border-b px-4 py-2">
<Button <Button
variant={"destructive"} variant={"destructive"}
onClick={() => deleteLog(log._id)} onClick={() => deleteLog(log._id)}
> >
Delete Delete
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</section> </section>
</section> </section>
); );
}; };
export default AdminLogPage; export default AdminLogPage;

View file

@ -8,140 +8,140 @@ import { EXAMPLE_A1 } from "@/constants";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
const EmailEditor = () => { const EmailEditor = () => {
const { toast } = useToast(); const { toast } = useToast();
const [subject, setSubject] = useState(""); const [subject, setSubject] = useState("");
const [previewContent, setPreviewContent] = useState<string>(EXAMPLE_A1); const [previewContent, setPreviewContent] = useState<string>(EXAMPLE_A1);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const validateInputs = () => { const validateInputs = () => {
if (!subject.trim() || !previewContent.trim()) { if (!subject.trim() || !previewContent.trim()) {
toast({ toast({
title: "Validation Error", title: "Validation Error",
description: "Subject and content cannot be empty.", description: "Subject and content cannot be empty.",
variant: "destructive", variant: "destructive"
}); });
return false; return false;
} }
return true; return true;
}; };
const handleSendAll = async () => { const handleSendAll = async () => {
if (!validateInputs()) return; if (!validateInputs()) return;
setLoading(true); setLoading(true);
try { try {
const response = await fetch("/api/newsletter/send", { const response = await fetch("/api/newsletter/send", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, },
body: JSON.stringify({ body: JSON.stringify({
subject: subject, subject: subject,
html: previewContent, html: previewContent
}), })
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Network response was not ok"); throw new Error("Network response was not ok");
} }
const result = await response.json(); const result = await response.json();
toast({ toast({
title: "Success!", title: "Success!",
description: result.message || "Emails sent successfully", description: result.message || "Emails sent successfully"
}); });
} catch (error) { } catch (error) {
console.error("Error:", error); console.error("Error:", error);
toast({ toast({
title: "Uh oh!", title: "Uh oh!",
description: `Failed to send emails: ${error}`, description: `Failed to send emails: ${error}`,
variant: "destructive", variant: "destructive"
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleSendTest = async () => { const handleSendTest = async () => {
if (!validateInputs()) return; if (!validateInputs()) return;
setLoading(true); setLoading(true);
try { try {
const response = await fetch("/api/newsletter/test", { const response = await fetch("/api/newsletter/test", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, },
body: JSON.stringify({ body: JSON.stringify({
subject: subject, subject: subject,
html: previewContent, html: previewContent
}), })
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Network response was not ok"); throw new Error("Network response was not ok");
} }
const result = await response.json(); const result = await response.json();
toast({ toast({
title: "Success!", title: "Success!",
description: result.message || "Test email sent successfully", description: result.message || "Test email sent successfully"
}); });
} catch (error) { } catch (error) {
console.error("Error:", error); console.error("Error:", error);
toast({ toast({
title: "Uh oh!", title: "Uh oh!",
description: `Failed to send test email: ${error}`, description: `Failed to send test email: ${error}`,
variant: "destructive", variant: "destructive"
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleEditorChange = (value: string) => { const handleEditorChange = (value: string) => {
setPreviewContent(value); setPreviewContent(value);
}; };
return ( return (
<div className="flex flex-col lg:flex-row h-screen"> <div className="flex flex-col lg:flex-row h-screen">
<div className="w-full lg:w-1/2 p-4 flex flex-col space-y-4"> <div className="w-full lg:w-1/2 p-4 flex flex-col space-y-4">
<Link href="/admin/email" className="text-blue-500 underline"> <Link href="/admin/email" className="text-blue-500 underline">
Back Back
</Link> </Link>
<input <input
type="text" type="text"
placeholder="Subject" placeholder="Subject"
value={subject} value={subject}
onChange={(e) => setSubject(e.target.value)} onChange={(e) => setSubject(e.target.value)}
className="border rounded-md p-2" className="border rounded-md p-2"
/> />
<CodeEditor onChange={handleEditorChange} /> <CodeEditor onChange={handleEditorChange} />
<div className="flex space-x-2 mt-4"> <div className="flex space-x-2 mt-4">
<Button <Button
variant={"secondary"} variant={"secondary"}
onClick={handleSendTest} onClick={handleSendTest}
disabled={loading} disabled={loading}
> >
{loading ? "Sending..." : "Send Test"} {loading ? "Sending..." : "Send Test"}
</Button> </Button>
<Button onClick={handleSendAll} disabled={loading}> <Button onClick={handleSendAll} disabled={loading}>
{loading ? "Sending..." : "Send All"} {loading ? "Sending..." : "Send All"}
</Button> </Button>
</div> </div>
</div> </div>
<div className="w-full lg:w-1/2 p-4 overflow-auto"> <div className="w-full lg:w-1/2 p-4 overflow-auto">
<h2 className="text-2xl font-bold mb-4 text-secondary-foreground"> <h2 className="text-2xl font-bold mb-4 text-secondary-foreground">
Email Preview Email Preview
</h2> </h2>
<div <div
className="border rounded-md p-4" className="border rounded-md p-4"
dangerouslySetInnerHTML={{ __html: previewContent }} dangerouslySetInnerHTML={{ __html: previewContent }}
/> />
</div> </div>
</div> </div>
); );
}; };
export default EmailEditor; export default EmailEditor;

View file

@ -2,13 +2,9 @@ import React from "react";
import AuthProvider from "../../components/shared/providers/AuthProvider"; import AuthProvider from "../../components/shared/providers/AuthProvider";
export default function AdminLayout({ export default function AdminLayout({
children, children
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return <AuthProvider>{children}</AuthProvider>;
<AuthProvider>
{children}
</AuthProvider>
);
} }

View file

@ -19,7 +19,7 @@ import PrismLoader from "@/components/loader/prismLoader";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
async function getData(slug: string) { async function getData(slug: string) {
const query = ` const query = `
*[_type == "blog" && slug.current == '${slug}'] { *[_type == "blog" && slug.current == '${slug}'] {
"currentSlug": slug.current, "currentSlug": slug.current,
title, title,
@ -28,177 +28,177 @@ async function getData(slug: string) {
_createdAt _createdAt
}[0]`; }[0]`;
const data = await client.fetch(query); const data = await client.fetch(query);
return data; return data;
} }
interface BlogSlugArticle { interface BlogSlugArticle {
currentSlug: string; currentSlug: string;
title: string; title: string;
content: any; content: any;
titleImage: string; titleImage: string;
_createdAt: string; _createdAt: string;
} }
export async function generateMetadata({ export async function generateMetadata({
params, params
}: { }: {
params: { slug: string }; params: { slug: string };
}): Promise<Metadata> { }): Promise<Metadata> {
const data = await getData(params.slug); const data = await getData(params.slug);
if (!data) { if (!data) {
return { return {
title: "Not Found", title: "Not Found",
description: "Blog post not found", description: "Blog post not found"
}; };
} }
return { return {
title: `${data.title} - SVRJS`, title: `${data.title} - SVRJS`,
description: data.smallDescription, description: data.smallDescription,
openGraph: { openGraph: {
title: `${data.title} - SVRJS`, title: `${data.title} - SVRJS`,
description: data.smallDescription, description: data.smallDescription,
url: `https://svrjs.org/blog/${data.currentSlug}`, url: `https://svrjs.org/blog/${data.currentSlug}`,
type: "website", type: "website",
images: [ images: [
{ {
url: urlFor(data.titleImage).url(), url: urlFor(data.titleImage).url(),
width: 800, width: 800,
height: 600, height: 600,
alt: `${data.title} - SVRJS`, alt: `${data.title} - SVRJS`
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: `${data.title} - SVRJS`, title: `${data.title} - SVRJS`,
description: data.smallDescription, description: data.smallDescription,
images: [urlFor(data.titleImage).url()], images: [urlFor(data.titleImage).url()],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
} }
const customPortableTextComponents: PortableTextComponents = { const customPortableTextComponents: PortableTextComponents = {
types: { types: {
image: ({ value }) => { image: ({ value }) => {
return ( return (
<div className="my-8"> <div className="my-8">
<Image <Image
src={urlFor(value).url()} src={urlFor(value).url()}
alt={value.alt || "Blog Image"} alt={value.alt || "Blog Image"}
width={1200} width={1200}
height={800} height={800}
className="w-full h-auto rounded-lg" className="w-full h-auto rounded-lg"
/> />
{value.caption && ( {value.caption && (
<p className="mt-2 text-center text-sm text-muted-foreground"> <p className="mt-2 text-center text-sm text-muted-foreground">
{value.caption} {value.caption}
</p> </p>
)} )}
</div> </div>
); );
}, },
code: ({ value }) => { code: ({ value }) => {
const language = value.language || "javascript"; const language = value.language || "javascript";
const grammar = Prism.languages[language]; const grammar = Prism.languages[language];
if (!grammar) { if (!grammar) {
console.error(`No grammar found for language: "${language}"`); console.error(`No grammar found for language: "${language}"`);
return ( return (
<pre className="p-4 rounded-md overflow-x-auto text-sm"> <pre className="p-4 rounded-md overflow-x-auto text-sm">
<code>{value.code}</code> <code>{value.code}</code>
</pre> </pre>
); );
} }
return ( return (
<div className="relative my-8"> <div className="relative my-8">
<pre <pre
className={`language-${language} p-4 rounded-md overflow-x-auto text-sm`} className={`language-${language} p-4 rounded-md overflow-x-auto text-sm`}
> >
<code className={`language-${language}`}>{value.code}</code> <code className={`language-${language}`}>{value.code}</code>
</pre> </pre>
<PrismLoader /> <PrismLoader />
<CopyButton code={value.code} /> <CopyButton code={value.code} />
</div> </div>
); );
}, }
}, }
}; };
export default async function BlogSlugArticle({ export default async function BlogSlugArticle({
params, params
}: { }: {
params: { slug: string }; params: { slug: string };
}) { }) {
const data: BlogSlugArticle = await getData(params.slug); const data: BlogSlugArticle = await getData(params.slug);
if (!data) { if (!data) {
notFound(); notFound();
} }
const formattedDate = format(new Date(data._createdAt), "MMMM d, yyyy"); const formattedDate = format(new Date(data._createdAt), "MMMM d, yyyy");
return ( return (
<> <>
<section className="max-w-5xl container mx-auto py-8 md:py-28 flex flex-col items-center px-4"> <section className="max-w-5xl container mx-auto py-8 md:py-28 flex flex-col items-center px-4">
<div className="w-full mx-auto flex-center"> <div className="w-full mx-auto flex-center">
<Link <Link
href="/blog" href="/blog"
className="group text-primary transition-all flex items-center" className="group text-primary transition-all flex items-center"
> >
<Button variant={"ghost"} size={"lg"} className="mx-0 px-2 "> <Button variant={"ghost"} size={"lg"} className="mx-0 px-2 ">
<ArrowLeft className="mr-2 w-5 h-5 group-hover:translate-x-1 transition-all" /> <ArrowLeft className="mr-2 w-5 h-5 group-hover:translate-x-1 transition-all" />
Back Back
</Button> </Button>
</Link> </Link>
<Link <Link
href="/rss.xml" href="/rss.xml"
className="ml-auto" className="ml-auto"
rel="alternate" rel="alternate"
type="application/rss+xml" type="application/rss+xml"
> >
<Button <Button
variant={"link"} variant={"link"}
size={"lg"} size={"lg"}
className="mx-0 px-2 text-accent-foreground" className="mx-0 px-2 text-accent-foreground"
> >
<Rss className="w-5 h-5 mr-1" /> Subscribe to RSS <Rss className="w-5 h-5 mr-1" /> Subscribe to RSS
</Button> </Button>
</Link> </Link>
</div> </div>
<header className="text-start mb-8 w-full"> <header className="text-start mb-8 w-full">
{data.titleImage && ( {data.titleImage && (
<div className="mb-2"> <div className="mb-2">
<h1 className="text-3xl md:text-5xl mb-8 py-4 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400"> <h1 className="text-3xl md:text-5xl mb-8 py-4 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
{data.title} {data.title}
</h1> </h1>
<Image <Image
src={urlFor(data.titleImage).url()} src={urlFor(data.titleImage).url()}
alt={data.title} alt={data.title}
width={1200} width={1200}
height={800} height={800}
priority priority
className="w-full h-auto object-cover rounded-md" className="w-full h-auto object-cover rounded-md"
/> />
<p className="mt-4 text-lg md:text-xl text-muted-foreground"> <p className="mt-4 text-lg md:text-xl text-muted-foreground">
Uploaded at {formattedDate} Uploaded at {formattedDate}
</p> </p>
</div> </div>
)} )}
</header> </header>
<Separator className="mb-6" /> <Separator className="mb-6" />
<article className="prose max-w-full md:prose-lg dark:prose-invert"> <article className="prose max-w-full md:prose-lg dark:prose-invert">
<PortableText <PortableText
value={data.content} value={data.content}
components={customPortableTextComponents} components={customPortableTextComponents}
/> />
</article> </article>
</section> </section>
</> </>
); );
} }

View file

@ -6,61 +6,61 @@ import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Blog - SVRJS", title: "Blog - SVRJS",
description: description:
"Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.", "Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.",
openGraph: { openGraph: {
title: "Blog - SVRJS", title: "Blog - SVRJS",
description: description:
"Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.", "Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.",
url: "https://svrjs.org/blog", url: "https://svrjs.org/blog",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "Blog - SVRJS", alt: "Blog - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "Blog - SVRJS", title: "Blog - SVRJS",
description: description:
"Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.", "Welcome to the SVR.JS Blog! Explore our latest blog posts featuring web development, web application security, and web server administration tips. Stay tuned for the latest SVR.JS updates.",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
const BlogPage = async ({ const BlogPage = async ({
searchParams, searchParams
}: { }: {
searchParams: { page?: string }; searchParams: { page?: string };
}) => { }) => {
// Optionally, you can fetch some initial data here if needed. // Optionally, you can fetch some initial data here if needed.
return ( return (
<section <section
id="blog" id="blog"
className="wrapper container py-24 md:py-28 gap-2 flex-center flex-col" className="wrapper container py-24 md:py-28 gap-2 flex-center flex-col"
> >
<h1 className="text-3xl md:text-5xl mb-3 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"> <h1 className="text-3xl md:text-5xl mb-3 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 Blog Post SVRJS Blog Post
</h1> </h1>
<p className="text-muted-foreground flex-center mb-2"> <p className="text-muted-foreground flex-center mb-2">
Stay updated with our latest blog posts by subscribing to our Stay updated with our latest blog posts by subscribing to our
<Link href="/rss.xml" rel="alternate" type="application/rss+xml"> <Link href="/rss.xml" rel="alternate" type="application/rss+xml">
<Button variant={"link"} className="mx-0 px-2"> <Button variant={"link"} className="mx-0 px-2">
<Rss className="w-5 h-5 mr-1" /> RSS feed <Rss className="w-5 h-5 mr-1" /> RSS feed
</Button> </Button>
</Link> </Link>
</p> </p>
<BlogCards searchParams={searchParams} /> <BlogCards searchParams={searchParams} />
</section> </section>
); );
}; };
export default BlogPage; export default BlogPage;

View file

@ -5,126 +5,126 @@ import ReactMarkdown from "react-markdown";
import Head from "next/head"; import Head from "next/head";
const Page = ({ params }: { params: { slug: string } }) => { const Page = ({ params }: { params: { slug: string } }) => {
const { slug } = params; const { slug } = params;
const [page, setPage] = useState<{ title: string; content: string } | null>( const [page, setPage] = useState<{ title: string; content: string } | null>(
null null
); );
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false); const [notFound, setNotFound] = useState(false);
useEffect(() => { useEffect(() => {
const fetchPage = async () => { const fetchPage = async () => {
try { try {
const response = await fetch(`/api/mdx/pages/${slug}`); const response = await fetch(`/api/mdx/pages/${slug}`);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setPage(data); setPage(data);
return (document.title = `${data.title} Change Log - SVRJS`); return (document.title = `${data.title} Change Log - SVRJS`);
} else { } else {
if (response.status === 404) { if (response.status === 404) {
setNotFound(true); setNotFound(true);
return (document.title = "404 Not Found"); return (document.title = "404 Not Found");
} }
} }
} catch (error) { } catch (error) {
console.error("Failed to load page", error); console.error("Failed to load page", error);
setNotFound(true); setNotFound(true);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
fetchPage(); fetchPage();
}, [slug]); }, [slug]);
if (loading) { if (loading) {
return ( return (
<> <>
<head> <head>
<title>Mods Change Logs - SVRJS</title> <title>Mods Change Logs - SVRJS</title>
</head> </head>
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col"> <section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
<div className="mb-3"> <div className="mb-3">
<Skeleton className="w-[400px] h-[50px] rounded-md" /> <Skeleton className="w-[400px] h-[50px] rounded-md" />
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Skeleton className="w-[300px] h-[30px] rounded-md" /> <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" /> <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> </div>
</section> </section>
</> </>
); );
} }
if (notFound) { if (notFound) {
return ( return (
<section id="404error" className="flex-center flex-col wrapper container"> <section id="404error" className="flex-center flex-col wrapper container">
<h1 className="text-3xl md:text-5xl text-center"> <h1 className="text-3xl md:text-5xl text-center">
<span className="text-red-500">404</span> Page not Found <span className="text-red-500">404</span> Page not Found
</h1> </h1>
<p className="text-lg mt-3 text-muted-foreground"> <p className="text-lg mt-3 text-muted-foreground">
Please return back to Home Please return back to Home
</p> </p>
</section> </section>
); );
} }
if (!page) { if (!page) {
return null; return null;
} }
return ( return (
<> <>
<head> <head>
<title>{page.title} Changelog - SVRJS</title> <title>{page.title} Changelog - SVRJS</title>
<meta <meta
name="description" name="description"
content={`Keep track of the latest updates and improvements for ${page.title} with our comprehensive change log. Discover new features, bug fixes, and enhancements for each release of this SVR.JS mod.`} content={`Keep track of the latest updates and improvements for ${page.title} with our comprehensive change log. Discover new features, bug fixes, and enhancements for each release of this SVR.JS mod.`}
/> />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content={`${page.title} Changelog - SVRJS`} /> <meta property="og:title" content={`${page.title} Changelog - SVRJS`} />
<meta <meta
property="og:description" property="og:description"
content={`Keep track of the latest updates and improvements for ${page.title} with our comprehensive change log. Discover new features, bug fixes, and enhancements for each release of this SVR.JS mod.`} content={`Keep track of the latest updates and improvements for ${page.title} with our comprehensive change log. Discover new features, bug fixes, and enhancements for each release of this SVR.JS mod.`}
/> />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta <meta
property="og:url" property="og:url"
content={`https://svrjs.org/changelogs/${slug}`} content={`https://svrjs.org/changelogs/${slug}`}
/> />
<meta <meta
property="og:image" property="og:image"
content="https://svrjs.vercel.app/metadata/svrjs-cover.png" content="https://svrjs.vercel.app/metadata/svrjs-cover.png"
/> />
<title>Documentation - SVRJS</title> <title>Documentation - SVRJS</title>
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta <meta
name="twitter:title" name="twitter:title"
content={`${page.title} Changelog - SVRJS`} content={`${page.title} Changelog - SVRJS`}
/> />
<meta <meta
name="twitter:description" name="twitter:description"
content={`Keep track of the latest updates and improvements for ${page.title} with our comprehensive change log. Discover new features, bug fixes, and enhancements for each release of this SVR.JS mod.`} content={`Keep track of the latest updates and improvements for ${page.title} with our comprehensive change log. Discover new features, bug fixes, and enhancements for each release of this SVR.JS mod.`}
/> />
<meta <meta
name="twitter:image" name="twitter:image"
content="https://svrjs.vercel.app/metadata/svrjs-cover.png" content="https://svrjs.vercel.app/metadata/svrjs-cover.png"
/> />
</head> </head>
<section className="wrapper container py-24 md:py-28 gap-2 flex flex-col"> <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"> <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} Change Log {page.title} Change Log
</h1> </h1>
<ReactMarkdown className="prose max-w-full md:prose-lg dark:prose-invert"> <ReactMarkdown className="prose max-w-full md:prose-lg dark:prose-invert">
{page.content} {page.content}
</ReactMarkdown> </ReactMarkdown>
</section> </section>
</> </>
); );
}; };
export default Page; export default Page;

View file

@ -2,36 +2,36 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "ChangeLogs - SVRJS", title: "ChangeLogs - SVRJS",
description: description:
"Stay up-to-date with the latest improvements and updates to SVR.JS web server. Our change log page provides a comprehensive list of new features, bug fixes, and enhancements for each release.", "Stay up-to-date with the latest improvements and updates to SVR.JS web server. Our change log page provides a comprehensive list of new features, bug fixes, and enhancements for each release.",
openGraph: { openGraph: {
title: "ChangeLogs - SVRJS", title: "ChangeLogs - SVRJS",
description: description:
"Stay up-to-date with the latest improvements and updates to SVR.JS web server. Our change log page provides a comprehensive list of new features, bug fixes, and enhancements for each release.", "Stay up-to-date with the latest improvements and updates to SVR.JS web server. Our change log page provides a comprehensive list of new features, bug fixes, and enhancements for each release.",
url: "https://svrjs.org/changelogs", url: "https://svrjs.org/changelogs",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "ChangeLogs - SVRJS", alt: "ChangeLogs - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "ChangeLogs - SVRJS", title: "ChangeLogs - SVRJS",
description: description:
"Stay up-to-date with the latest improvements and updates to SVR.JS web server. Our change log page provides a comprehensive list of new features, bug fixes, and enhancements for each release.", "Stay up-to-date with the latest improvements and updates to SVR.JS web server. Our change log page provides a comprehensive list of new features, bug fixes, and enhancements for each release.",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
const ContactLayout = ({ children }: { children: React.ReactNode }) => { const ContactLayout = ({ children }: { children: React.ReactNode }) => {
return <main>{children}</main>; return <main>{children}</main>;
}; };
export default ContactLayout; export default ContactLayout;

View file

@ -9,103 +9,103 @@ import { CHANGE_LOGS } from "@/constants/guidelines";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
interface Bullet { interface Bullet {
point: string; point: string;
} }
interface LOGS { interface LOGS {
_id: string; _id: string;
date: string; date: string;
version: string; version: string;
bullets?: Bullet[]; // Make bullets optional bullets?: Bullet[]; // Make bullets optional
} }
const LogsPage: React.FC = () => { const LogsPage: React.FC = () => {
const [downloads, setDownloads] = useState<LOGS[]>([]); const [downloads, setDownloads] = useState<LOGS[]>([]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const fetchDownloads = async () => { const fetchDownloads = async () => {
try { try {
const response = await fetch("/api/logs", { const response = await fetch("/api/logs", {
method: "GET", method: "GET"
}); });
if (response.ok) { if (response.ok) {
const data: LOGS[] = await response.json(); const data: LOGS[] = await response.json();
setDownloads(data); setDownloads(data);
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to fetch downloads"); setError(error.message || "Failed to fetch downloads");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
fetchDownloads(); fetchDownloads();
const interval = setInterval(() => { const interval = setInterval(() => {
fetchDownloads(); fetchDownloads();
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const reversedDownloads = [...downloads].reverse(); const reversedDownloads = [...downloads].reverse();
if (loading) { if (loading) {
return ( return (
<> <>
<head> <head>
<title>Change Logs - SVRJS</title> <title>Change Logs - SVRJS</title>
</head> </head>
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col"> <section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
<div className="mb-3"> <div className="mb-3">
<Skeleton className="w-[400px] h-[50px] rounded-md" /> <Skeleton className="w-[400px] h-[50px] rounded-md" />
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Skeleton className="w-[300px] h-[30px] rounded-md" /> <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" /> <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> </div>
</section> </section>
</> </>
); );
} }
return ( return (
<section <section
id="logs" id="logs"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col" 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"> <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 Server LOGS
</h1> </h1>
<p className="md:text-lg text-muted-foreground text-start mb-6"> <p className="md:text-lg text-muted-foreground text-start mb-6">
Get all the latest version of SVRJS download and compiled Files here! Get all the latest version of SVRJS download and compiled Files here!
</p> </p>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
{reversedDownloads.map((download) => ( {reversedDownloads.map((download) => (
<div <div
key={download._id} key={download._id}
className="flex-start prose max-w-full md:prose-lg dark:prose-invert flex-col mb-4" 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> <h2 className="font-bold text-3xl">{download.version}</h2>
<span className="font-medium italic">{download.date}</span> <span className="font-medium italic">{download.date}</span>
<ul className="list-disc pl-5"> <ul className="list-disc pl-5">
{(download.bullets ?? []).map((bullet, index) => ( {(download.bullets ?? []).map((bullet, index) => (
<li key={index}>{bullet.point}</li> <li key={index}>{bullet.point}</li>
))} ))}
</ul> </ul>
</div> </div>
))} ))}
<div className="prose max-w-full md:prose-lg dark:prose-invert"> <div className="prose max-w-full md:prose-lg dark:prose-invert">
<ReactMarkdown>{CHANGE_LOGS}</ReactMarkdown> <ReactMarkdown>{CHANGE_LOGS}</ReactMarkdown>
</div> </div>
</section> </section>
); );
}; };
export default LogsPage; export default LogsPage;

View file

@ -2,36 +2,36 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Contact Us - SVRJS", title: "Contact Us - SVRJS",
description: description:
"Have questions about SVR.JS? Need technical support? Visit our Contact Us page to find various ways to get in touch with our team, including email, forums, and our official support channel.", "Have questions about SVR.JS? Need technical support? Visit our Contact Us page to find various ways to get in touch with our team, including email, forums, and our official support channel.",
openGraph: { openGraph: {
title: "Contact Us - SVRJS", title: "Contact Us - SVRJS",
description: description:
"Have questions about SVR.JS? Need technical support? Visit our Contact Us page to find various ways to get in touch with our team, including email, forums, and our official support channel.", "Have questions about SVR.JS? Need technical support? Visit our Contact Us page to find various ways to get in touch with our team, including email, forums, and our official support channel.",
url: "https://svrjs.org/contact", url: "https://svrjs.org/contact",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "Contact Us - SVRJS", alt: "Contact Us - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "Contact Us - SVRJS", title: "Contact Us - SVRJS",
description: description:
"Have questions about SVR.JS? Need technical support? Visit our Contact Us page to find various ways to get in touch with our team, including email, forums, and our official support channel.", "Have questions about SVR.JS? Need technical support? Visit our Contact Us page to find various ways to get in touch with our team, including email, forums, and our official support channel.",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
const ContactLayout = ({ children }: { children: React.ReactNode }) => { const ContactLayout = ({ children }: { children: React.ReactNode }) => {
return <main>{children}</main>; return <main>{children}</main>;
}; };
export default ContactLayout; export default ContactLayout;

View file

@ -7,12 +7,12 @@ import { z } from "zod";
import { contactFormSchema } from "@/lib/validations/validation"; import { contactFormSchema } from "@/lib/validations/validation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -23,179 +23,179 @@ import { emails } from "@/constants";
import HCaptcha from "@hcaptcha/react-hcaptcha"; import HCaptcha from "@hcaptcha/react-hcaptcha";
const ContactUs = () => { const ContactUs = () => {
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showCaptcha, setShowCaptcha] = useState(false); const [showCaptcha, setShowCaptcha] = useState(false);
const [captchaToken, setCaptchaToken] = useState<string | null>(null); const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const form = useForm<z.infer<typeof contactFormSchema>>({ const form = useForm<z.infer<typeof contactFormSchema>>({
resolver: zodResolver(contactFormSchema), resolver: zodResolver(contactFormSchema),
defaultValues: { defaultValues: {
name: "", name: "",
email: "", email: "",
message: "", message: ""
}, }
}); });
async function onSubmit(values: z.infer<typeof contactFormSchema>) { async function onSubmit(values: z.infer<typeof contactFormSchema>) {
if (!captchaToken) { if (!captchaToken) {
setShowCaptcha(true); setShowCaptcha(true);
return; return;
} }
setLoading(true); setLoading(true);
try { try {
const res = await fetch("/api/contact", { const res = await fetch("/api/contact", {
method: "POST", method: "POST",
body: JSON.stringify({ ...values, captchaToken }), body: JSON.stringify({ ...values, captchaToken }),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json"
}, }
}); });
if (res.ok) { if (res.ok) {
form.reset(); form.reset();
setCaptchaToken(null); // Reset captcha token after successful submission setCaptchaToken(null); // Reset captcha token after successful submission
toast({ toast({
description: "Your message has been sent.", description: "Your message has been sent."
}); });
} else { } else {
toast({ toast({
title: "Uh oh! Something went wrong.", title: "Uh oh! Something went wrong.",
variant: "destructive", variant: "destructive"
}); });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast({ toast({
title: "Uh oh! Something went wrong.", title: "Uh oh! Something went wrong.",
variant: "destructive", variant: "destructive"
}); });
} finally { } finally {
setLoading(false); setLoading(false);
setShowCaptcha(false); // Hide captcha after submission attempt setShowCaptcha(false); // Hide captcha after submission attempt
} }
} }
function handleCaptchaVerify(token: string) { function handleCaptchaVerify(token: string) {
setCaptchaToken(token); setCaptchaToken(token);
onSubmit(form.getValues()); // Trigger form submission after captcha is verified onSubmit(form.getValues()); // Trigger form submission after captcha is verified
} }
return ( return (
<> <>
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300"> <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"> <h1 className="text-4xl md:text-6xl tracking-tight font-bold uppercase text-center text-gray-900 dark:text-white">
Contact Us Contact Us
</h1> </h1>
</div> </div>
<section id="contact" className="w-full"> <section id="contact" className="w-full">
<div className="flex-center flex-col md:flex-row justify-between mx-auto p-6 max-w-5xl"> <div className="flex-center flex-col md:flex-row justify-between mx-auto p-6 max-w-5xl">
{/* Left contact page */} {/* Left contact page */}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} 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" className="space-y-4 pb-8 mb-8 max-w-lg w-full bg-accent border p-6 rounded-lg shadow-md"
> >
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Your Name" {...field} /> <Input placeholder="Your Name" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<FormControl> <FormControl>
<Input type="email" placeholder="Your Email" {...field} /> <Input type="email" placeholder="Your Email" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="message" name="message"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Message</FormLabel> <FormLabel>Message</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
className="h-44" className="h-44"
placeholder="Your Message" placeholder="Your Message"
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{showCaptcha && ( {showCaptcha && (
<HCaptcha <HCaptcha
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!} sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
onVerify={handleCaptchaVerify} onVerify={handleCaptchaVerify}
/> />
)} )}
<Button <Button
type="submit" type="submit"
variant={"default"} variant={"default"}
className="w-full mt-2" className="w-full mt-2"
disabled={loading} disabled={loading}
> >
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<span className="tracking-tight font-semibold">SEND</span> <span className="tracking-tight font-semibold">SEND</span>
<Send className="ml-2 w-5 h-5" /> <Send className="ml-2 w-5 h-5" />
</div> </div>
</Button> </Button>
</form> </form>
</Form> </Form>
{/* Right contact page */} {/* Right contact page */}
<div className="max-w-lg mt-8 md:mt-0 md:ml-8 p-12 border rounded-lg"> <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"> <ul className="space-y-4 mb-6">
{emails.map((email, index) => ( {emails.map((email, index) => (
<li <li
key={index} key={index}
className="text-gray-600 dark:text-gray-300 flex items-center" className="text-gray-600 dark:text-gray-300 flex items-center"
> >
<email.icon className="mr-2" size={24} /> <email.icon className="mr-2" size={24} />
<span> <span>
<a <a
href={email.url} href={email.url}
title={`Send an email to ${email.email}`} title={`Send an email to ${email.email}`}
className="text-muted-foreground hover:text-accent-foreground transition duration-200" className="text-muted-foreground hover:text-accent-foreground transition duration-200"
> >
{email.email} {email.email}
</a> </a>
</span> </span>
</li> </li>
))} ))}
</ul> </ul>
<Separator /> <Separator />
<ul className="flex justify-center space-x-3 my-6"> <ul className="flex justify-center space-x-3 my-6">
<Iconss /> <Iconss />
</ul> </ul>
<Separator /> <Separator />
<div className="text-center text-gray-500 mt-2 text-sm font-light"></div> <div className="text-center text-gray-500 mt-2 text-sm font-light"></div>
</div> </div>
</div> </div>
</section> </section>
</> </>
); );
}; };
export default ContactUs; export default ContactUs;

View file

@ -4,52 +4,52 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Contribute - SVRJS", title: "Contribute - SVRJS",
description: description:
"Contribute to SVR.JS and be part of an exciting open-source project. Follow the step-by-step guidelines to make your code contributions.", "Contribute to SVR.JS and be part of an exciting open-source project. Follow the step-by-step guidelines to make your code contributions.",
openGraph: { openGraph: {
title: "Contribute - SVRJS", title: "Contribute - SVRJS",
description: description:
"Contribute to SVR.JS and be part of an exciting open-source project. Follow the step-by-step guidelines to make your code contributions.", "Contribute to SVR.JS and be part of an exciting open-source project. Follow the step-by-step guidelines to make your code contributions.",
url: "https://svrjs.org/contribute", url: "https://svrjs.org/contribute",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "Contribute - SVRJS", alt: "Contribute - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "Contribute - SVRJS", title: "Contribute - SVRJS",
description: description:
"Contribute to SVR.JS and be part of an exciting open-source project. Follow the step-by-step guidelines to make your code contributions.", "Contribute to SVR.JS and be part of an exciting open-source project. Follow the step-by-step guidelines to make your code contributions.",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
const Contribute = () => { const Contribute = () => {
return ( return (
<section <section
id="tos" id="tos"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col" 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"> <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 Contributing to SVR.JS
</h1> </h1>
<p className="md:text-lg text-muted-foreground text-start mb-6"> <p className="md:text-lg text-muted-foreground text-start mb-6">
We welcome contributions from the community! Here&apos;s how you can We welcome contributions from the community! Here&apos;s how you can
help! help!
</p> </p>
<div className="prose max-w-full md:prose-lg dark:prose-invert"> <div className="prose max-w-full md:prose-lg dark:prose-invert">
<ReactMarkdown>{contribute}</ReactMarkdown> <ReactMarkdown>{contribute}</ReactMarkdown>
</div> </div>
</section> </section>
); );
}; };
export default Contribute; export default Contribute;

View file

@ -2,39 +2,39 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Downloads - SVRJS", title: "Downloads - SVRJS",
description: description:
"Ready to get started with SVR.JS? Visit our downloads page to access the latest stable releases, nightly builds, and archived versions. Find the right fit for your needs today!", "Ready to get started with SVR.JS? Visit our downloads page to access the latest stable releases, nightly builds, and archived versions. Find the right fit for your needs today!",
openGraph: { openGraph: {
title: "Downloads - SVRJS", title: "Downloads - SVRJS",
description: description:
"Ready to get started with SVR.JS? Visit our downloads page to access the latest stable releases, nightly builds, and archived versions. Find the right fit for your needs today!", "Ready to get started with SVR.JS? Visit our downloads page to access the latest stable releases, nightly builds, and archived versions. Find the right fit for your needs today!",
url: "https://svrjs.org/downloads", url: "https://svrjs.org/downloads",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "Downloads - SVRJS", alt: "Downloads - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "Downloads - SVRJS", title: "Downloads - SVRJS",
description: description:
"Ready to get started with SVR.JS? Visit our downloads page to access the latest stable releases, nightly builds, and archived versions. Find the right fit for your needs today!", "Ready to get started with SVR.JS? Visit our downloads page to access the latest stable releases, nightly builds, and archived versions. Find the right fit for your needs today!",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
export default function DownloadLayout({ export default function DownloadLayout({
children, children
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return <main>{children}</main>; return <main>{children}</main>;
} }

View file

@ -3,111 +3,111 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption, TableCaption,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
interface Download { interface Download {
_id: string; _id: string;
date: string; date: string;
fileName: string; fileName: string;
version: string; version: string;
fileSize: string; fileSize: string;
downloadLink?: string; // Optional downloadLink?: string; // Optional
} }
const DownloadPage: React.FC = () => { const DownloadPage: React.FC = () => {
const [downloads, setDownloads] = useState<Download[]>([]); const [downloads, setDownloads] = useState<Download[]>([]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const fetchDownloads = async () => { const fetchDownloads = async () => {
try { try {
const response = await fetch("/api/downloads", { const response = await fetch("/api/downloads", {
method: "GET", method: "GET"
}); });
if (response.ok) { if (response.ok) {
const data: Download[] = await response.json(); const data: Download[] = await response.json();
setDownloads(data); setDownloads(data);
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message); setError(error.message);
} }
}; };
useEffect(() => { useEffect(() => {
fetchDownloads(); fetchDownloads();
const interval = setInterval(() => { const interval = setInterval(() => {
fetchDownloads(); fetchDownloads();
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
return ( return (
<section <section
id="download" id="download"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col" 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"> <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 Downloads
</h1> </h1>
<p className="md:text-lg text-muted-foreground text-start mb-6"> <p className="md:text-lg text-muted-foreground text-start mb-6">
Get all the latest version of SVRJS download and compiled Files here! Get all the latest version of SVRJS download and compiled Files here!
</p> </p>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
<Table> <Table>
<TableCaption>A list of all available downloads.</TableCaption> <TableCaption>A list of all available downloads.</TableCaption>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="w-[150px]">Date</TableHead> <TableHead className="w-[150px]">Date</TableHead>
<TableHead>File Name</TableHead> <TableHead>File Name</TableHead>
<TableHead>Version</TableHead> <TableHead>Version</TableHead>
<TableHead>File Size</TableHead> <TableHead>File Size</TableHead>
<TableHead className="text-right">Download Link</TableHead> <TableHead className="text-right">Download Link</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{downloads {downloads
.slice(0, 10) .slice(0, 10)
.reverse() .reverse()
.map((download) => ( .map((download) => (
<TableRow key={download._id}> <TableRow key={download._id}>
<TableCell className="font-medium">{download.date}</TableCell> <TableCell className="font-medium">{download.date}</TableCell>
<TableCell>{download.fileName}</TableCell> <TableCell>{download.fileName}</TableCell>
<TableCell>{download.version}</TableCell> <TableCell>{download.version}</TableCell>
<TableCell className="text-left">{download.fileSize}</TableCell> <TableCell className="text-left">{download.fileSize}</TableCell>
<TableCell className="flex items-center justify-end"> <TableCell className="flex items-center justify-end">
{download.downloadLink ? ( {download.downloadLink ? (
<Link href={download.downloadLink}> <Link href={download.downloadLink}>
<Button variant={"ghost"} className=""> <Button variant={"ghost"} className="">
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 mr-2" />
Download Download
</Button> </Button>
</Link> </Link>
) : ( ) : (
<Button variant={"ghost"} disabled> <Button variant={"ghost"} disabled>
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 mr-2" />
Unavailable Unavailable
</Button> </Button>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</section> </section>
); );
}; };
export default DownloadPage; export default DownloadPage;

View file

@ -3,11 +3,11 @@ import React from "react";
import { Metadata } from "next"; import { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Forum - SVRJS", title: "Forum - SVRJS"
}; };
const Forum = () => { const Forum = () => {
return <div>Forum</div>; return <div>Forum</div>;
}; };
export default Forum; export default Forum;

View file

@ -6,54 +6,54 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "SVRJS - A Web Server running on Node.js", title: "SVRJS - A Web Server running on Node.js",
description: 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).", "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: { openGraph: {
title: "SVRJS - A Web Server running on Node.js", title: "SVRJS - A Web Server running on Node.js",
description: 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).", "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", url: "https://svrjs.org",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "SVRJS - A Web Server running on Node.js", alt: "SVRJS - A Web Server running on Node.js"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "SVRJS - A Web Server running on Node.js", title: "SVRJS - A Web Server running on Node.js",
description: 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).", "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"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
export default function PageLayout({ export default function PageLayout({
children, children
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const iconClassName = "w-4 h-4 flex-center text-zinc-950 -mr-2"; const iconClassName = "w-4 h-4 flex-center text-zinc-950 -mr-2";
return ( return (
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
{/* Comment or edit this whenever required */} {/* Comment or edit this whenever required */}
<Banner <Banner
icon={<Home className={iconClassName} />} icon={<Home className={iconClassName} />}
title="SVR.JS 4.0.0 is now on beta!" title="SVR.JS 4.0.0 is now on beta!"
announcement="The latest beta version is SVR.JS 4.0.0-beta3." announcement="The latest beta version is SVR.JS 4.0.0-beta3."
link="https://blog.svrjs.org/2024/08/30/SVR-JS-4-0-0-beta3-has-been-released/" link="https://blog.svrjs.org/2024/08/30/SVR-JS-4-0-0-beta3-has-been-released/"
buttonText="Read more" buttonText="Read more"
/> />
<Navbar /> <Navbar />
<div className="flex-grow flex-1 overflow-x-hidden">{children}</div> <div className="flex-grow flex-1 overflow-x-hidden">{children}</div>
<Footer /> <Footer />
</div> </div>
); );
} }

View file

@ -2,36 +2,36 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Mods - SVRJS", title: "Mods - SVRJS",
description: description:
"Expand the functionality of SVR.JS with our collection of mods! Visit the mod downloads page to explore, download, and install a wide range of mods tailored to enhance your web server experience.", "Expand the functionality of SVR.JS with our collection of mods! Visit the mod downloads page to explore, download, and install a wide range of mods tailored to enhance your web server experience.",
openGraph: { openGraph: {
title: "Mods - SVRJS", title: "Mods - SVRJS",
description: description:
"Expand the functionality of SVR.JS with our collection of mods! Visit the mod downloads page to explore, download, and install a wide range of mods tailored to enhance your web server experience.", "Expand the functionality of SVR.JS with our collection of mods! Visit the mod downloads page to explore, download, and install a wide range of mods tailored to enhance your web server experience.",
url: "https://svrjs.org/mods", url: "https://svrjs.org/mods",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "Mods - SVRJS", alt: "Mods - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "Mods - SVRJS", title: "Mods - SVRJS",
description: description:
"Expand the functionality of SVR.JS with our collection of mods! Visit the mod downloads page to explore, download, and install a wide range of mods tailored to enhance your web server experience.", "Expand the functionality of SVR.JS with our collection of mods! Visit the mod downloads page to explore, download, and install a wide range of mods tailored to enhance your web server experience.",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
const ModLayout = ({ children }: { children: React.ReactNode }) => { const ModLayout = ({ children }: { children: React.ReactNode }) => {
return <main>{children}</main>; return <main>{children}</main>;
}; };
export default ModLayout; export default ModLayout;

View file

@ -9,7 +9,7 @@ import {
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@ -30,7 +30,7 @@ const ModsPage: React.FC = () => {
const fetchDownloads = async () => { const fetchDownloads = async () => {
try { try {
const response = await fetch("/api/mods", { const response = await fetch("/api/mods", {
method: "GET", method: "GET"
}); });
if (response.ok) { if (response.ok) {
const data: Mods[] = await response.json(); const data: Mods[] = await response.json();

View file

@ -2,7 +2,7 @@ import Newsletter from "@/components/shared/Newsletter";
import React from "react"; import React from "react";
const NewsletterPage = () => { const NewsletterPage = () => {
return <Newsletter />; return <Newsletter />;
}; };
export default NewsletterPage; export default NewsletterPage;

View file

@ -1,19 +1,19 @@
import Link from "next/link"; import Link from "next/link";
const NotFound = () => { const NotFound = () => {
return ( return (
<section id="404error" className="flex-center flex-col wrapper container"> <section id="404error" className="flex-center flex-col wrapper container">
<h1 className="text-3xl md:text-5xl text-center"> <h1 className="text-3xl md:text-5xl text-center">
<span className="text-red-500">404</span> Page not Found <span className="text-red-500">404</span> Page not Found
</h1> </h1>
<p className="text-lg mt-3 text-muted-foreground"> <p className="text-lg mt-3 text-muted-foreground">
Please return back to{" "} Please return back to{" "}
<Link href="/" className="underline font-bold"> <Link href="/" className="underline font-bold">
Home Home
</Link> </Link>
</p> </p>
</section> </section>
); );
}; };
export default NotFound; export default NotFound;

View file

@ -8,18 +8,18 @@ import Partners from "@/components/shared/Partners";
import Testimonials from "@/components/shared/Testimonials"; import Testimonials from "@/components/shared/Testimonials";
const RootPage = () => { const RootPage = () => {
return ( return (
<> <>
<Hero /> <Hero />
<HowItWorks /> <HowItWorks />
<Testimonials /> <Testimonials />
<Partners /> <Partners />
<About /> <About />
{/* <DataTable /> */} {/* <DataTable /> */}
<Faq /> <Faq />
<Newsletter /> <Newsletter />
</> </>
); );
}; };
export default RootPage; export default RootPage;

View file

@ -6,52 +6,52 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Privacy Policy - SVRJS", title: "Privacy Policy - SVRJS",
description: description:
"Learn how we collect, use, and protect your data. Our Privacy Policy outlines our commitment to your privacy and the measures we take to safeguard your information when visiting our website.", "Learn how we collect, use, and protect your data. Our Privacy Policy outlines our commitment to your privacy and the measures we take to safeguard your information when visiting our website.",
openGraph: { openGraph: {
title: "Privacy Policy - SVRJS", title: "Privacy Policy - SVRJS",
description: description:
"Learn how we collect, use, and protect your data. Our Privacy Policy outlines our commitment to your privacy and the measures we take to safeguard your information when visiting our website.", "Learn how we collect, use, and protect your data. Our Privacy Policy outlines our commitment to your privacy and the measures we take to safeguard your information when visiting our website.",
url: "https://svrjs.org/privacy-policy", url: "https://svrjs.org/privacy-policy",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "Privacy Policy - SVRJS", alt: "Privacy Policy - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "Privacy Policy - SVRJS", title: "Privacy Policy - SVRJS",
description: description:
"Learn how we collect, use, and protect your data. Our Privacy Policy outlines our commitment to your privacy and the measures we take to safeguard your information when visiting our website.", "Learn how we collect, use, and protect your data. Our Privacy Policy outlines our commitment to your privacy and the measures we take to safeguard your information when visiting our website.",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
const PrivacyPolicy = () => { const PrivacyPolicy = () => {
return ( return (
<section <section
id="privacy-policy" id="privacy-policy"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col" 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"> <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 Privacy Policy
</h1> </h1>
<p className="md:text-lg text-muted-foreground text-start mb-6"> <p className="md:text-lg text-muted-foreground text-start mb-6">
Effective date: 26.05.2024 Effective date: 26.05.2024
</p> </p>
<div className="prose max-w-full md:prose-lg dark:prose-invert"> <div className="prose max-w-full md:prose-lg dark:prose-invert">
<ReactMarkdown>{PRIVACY_POLICY}</ReactMarkdown> <ReactMarkdown>{PRIVACY_POLICY}</ReactMarkdown>
</div> </div>
</section> </section>
); );
}; };
export default PrivacyPolicy; export default PrivacyPolicy;

View file

@ -4,52 +4,52 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Terms of Service - SVRJS", title: "Terms of Service - SVRJS",
description: description:
"Understand your rights and responsibilities when using SVR.JS. Our Terms of Service page outlines the conditions for visiting our website, ensuring a transparent and fair experience for all users.", "Understand your rights and responsibilities when using SVR.JS. Our Terms of Service page outlines the conditions for visiting our website, ensuring a transparent and fair experience for all users.",
openGraph: { openGraph: {
title: "Terms of Service - SVRJS", title: "Terms of Service - SVRJS",
description: description:
"Understand your rights and responsibilities when using SVR.JS. Our Terms of Service page outlines the conditions for visiting our website, ensuring a transparent and fair experience for all users.", "Understand your rights and responsibilities when using SVR.JS. Our Terms of Service page outlines the conditions for visiting our website, ensuring a transparent and fair experience for all users.",
url: "https://svrjs.org/tos", url: "https://svrjs.org/tos",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "Terms of Service - SVRJS", alt: "Terms of Service - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "Terms of Service - SVRJS", title: "Terms of Service - SVRJS",
description: description:
"Understand your rights and responsibilities when using SVR.JS. Our Terms of Service page outlines the conditions for visiting our website, ensuring a transparent and fair experience for all users.", "Understand your rights and responsibilities when using SVR.JS. Our Terms of Service page outlines the conditions for visiting our website, ensuring a transparent and fair experience for all users.",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
const TermsOfService = () => { const TermsOfService = () => {
return ( return (
<section <section
id="tos" id="tos"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col" 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"> <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 Of Service Terms Of Service
</h1> </h1>
<p className="md:text-lg text-muted-foreground text-start mb-6"> <p className="md:text-lg text-muted-foreground text-start mb-6">
Last updated: 24.04.2024 Last updated: 24.04.2024
</p> </p>
<div className="prose max-w-full md:prose-lg dark:prose-invert"> <div className="prose max-w-full md:prose-lg dark:prose-invert">
<ReactMarkdown>{TERMS_AND_CONDITIONS}</ReactMarkdown> <ReactMarkdown>{TERMS_AND_CONDITIONS}</ReactMarkdown>
</div> </div>
</section> </section>
); );
}; };
export default TermsOfService; export default TermsOfService;

View file

@ -2,36 +2,36 @@ import { Metadata } from "next";
// baseURL [ENV] // baseURL [ENV]
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Vulnerabilities - SVRJS", title: "Vulnerabilities - SVRJS",
description: description:
"Learn about potential security risks associated with outdated SVR.JS web server versions. Stay informed and safeguard your web applications from potential threats with timely updates.", "Learn about potential security risks associated with outdated SVR.JS web server versions. Stay informed and safeguard your web applications from potential threats with timely updates.",
openGraph: { openGraph: {
title: "Vulnerabilities - SVRJS", title: "Vulnerabilities - SVRJS",
description: description:
"Learn about potential security risks associated with outdated SVR.JS web server versions. Stay informed and safeguard your web applications from potential threats with timely updates.", "Learn about potential security risks associated with outdated SVR.JS web server versions. Stay informed and safeguard your web applications from potential threats with timely updates.",
url: "https://svrjs.org/vulnerabilities", url: "https://svrjs.org/vulnerabilities",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "Vulnerabilities - SVRJS", alt: "Vulnerabilities - SVRJS"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "Vulnerabilities - SVRJS", title: "Vulnerabilities - SVRJS",
description: description:
"Learn about potential security risks associated with outdated SVR.JS web server versions. Stay informed and safeguard your web applications from potential threats with timely updates.", "Learn about potential security risks associated with outdated SVR.JS web server versions. Stay informed and safeguard your web applications from potential threats with timely updates.",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
const ModLayout = ({ children }: { children: React.ReactNode }) => { const ModLayout = ({ children }: { children: React.ReactNode }) => {
return <main>{children}</main>; return <main>{children}</main>;
}; };
export default ModLayout; export default ModLayout;

View file

@ -6,159 +6,159 @@ import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
interface Bullet { interface Bullet {
point: string; point: string;
} }
interface Vulnerabilities { interface Vulnerabilities {
_id: string; _id: string;
version: string; version: string;
bullets?: Bullet[]; // Make bullets optional bullets?: Bullet[]; // Make bullets optional
} }
interface ModsVulnerability { interface ModsVulnerability {
_id: string; _id: string;
title: string; title: string;
slug: string; slug: string;
content: string; content: string;
vulnerabilities: string; vulnerabilities: string;
} }
const Vulnerabilities = () => { const Vulnerabilities = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [downloads, setDownloads] = useState<Vulnerabilities[]>([]); const [downloads, setDownloads] = useState<Vulnerabilities[]>([]);
const [mods, setMods] = useState<ModsVulnerability[]>([]); const [mods, setMods] = useState<ModsVulnerability[]>([]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const fetchData = async () => { const fetchData = async () => {
try { try {
const response = await fetch("/api/vulnerabilities", { const response = await fetch("/api/vulnerabilities", {
method: "GET", method: "GET"
}); });
if (response.ok) { if (response.ok) {
const data: Vulnerabilities[] = await response.json(); const data: Vulnerabilities[] = await response.json();
setDownloads(data); setDownloads(data);
return (document.title = "Vulnerabilities - SVRJS"); return (document.title = "Vulnerabilities - SVRJS");
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to fetch downloads"); setError(error.message || "Failed to fetch downloads");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const fetchMods = async () => { const fetchMods = async () => {
try { try {
const response = await fetch(`/api/mdx/pages`, { const response = await fetch(`/api/mdx/pages`, {
method: "GET", method: "GET"
}); });
if (response.ok) { if (response.ok) {
const data: ModsVulnerability[] = await response.json(); const data: ModsVulnerability[] = await response.json();
// Filter out entries where vulnerabilities is undefined or an empty string // Filter out entries where vulnerabilities is undefined or an empty string
const filteredMods = data.filter( const filteredMods = data.filter(
(mod) => mod.vulnerabilities && mod.vulnerabilities.trim() !== "" (mod) => mod.vulnerabilities && mod.vulnerabilities.trim() !== ""
); );
setMods(filteredMods); setMods(filteredMods);
return (document.title = "Vulnerabilities - SVRJS"); return (document.title = "Vulnerabilities - SVRJS");
} else { } else {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
} catch (error: any) { } catch (error: any) {
setError(error.message || "Failed to fetch vulnerabilities"); setError(error.message || "Failed to fetch vulnerabilities");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
fetchMods(); fetchMods();
const interval = setInterval(() => { const interval = setInterval(() => {
fetchData(); fetchData();
fetchMods(); fetchMods();
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const reversedDownloads = [...downloads].reverse(); const reversedDownloads = [...downloads].reverse();
const reversedMods = [...mods].reverse(); const reversedMods = [...mods].reverse();
if (loading) { if (loading) {
return ( return (
<> <>
<head> <head>
<title>Vulnerabilities - SVRJS</title> <title>Vulnerabilities - SVRJS</title>
</head> </head>
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col"> <section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
<div className="mb-3"> <div className="mb-3">
<Skeleton className="w-[400px] h-[50px] rounded-md" /> <Skeleton className="w-[400px] h-[50px] rounded-md" />
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Skeleton className="w-[300px] h-[30px] rounded-md" /> <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" /> <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> </div>
</section> </section>
</> </>
); );
} }
return ( return (
<section <section
id="vulnerabilities" id="vulnerabilities"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col" 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"> <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 SVR.JS Vulnerabilities
</h1> </h1>
<p className="md:text-lg text-muted-foreground text-start mb-6"> <p className="md:text-lg text-muted-foreground text-start mb-6">
Some older versions of SVR.JS are vulnerable to cyberattacks. It&apos;s Some older versions of SVR.JS are vulnerable to cyberattacks. It&apos;s
recommended to update your SVR.JS version to the newest one. If you find 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 a security issue with SVR.JS, report it as soon as possible to
vulnerability-reports[at]svrjs[dot]org. We&apos;ll mitigate that vulnerability-reports[at]svrjs[dot]org. We&apos;ll mitigate that
vulnerability if it is possible. vulnerability if it is possible.
</p> </p>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
{reversedDownloads.map((download) => ( {reversedDownloads.map((download) => (
<div <div
key={download._id} key={download._id}
className="flex-start flex-col prose dark:prose-invert gap-4" className="flex-start flex-col prose dark:prose-invert gap-4"
> >
<h2 className="font-semibold text-3xl -mb-2">{download.version}</h2> <h2 className="font-semibold text-3xl -mb-2">{download.version}</h2>
<ul className="list-disc pl-5"> <ul className="list-disc pl-5">
{(download.bullets ?? []).map((bullet, index) => ( {(download.bullets ?? []).map((bullet, index) => (
<li key={index}>{bullet.point}</li> <li key={index}>{bullet.point}</li>
))} ))}
</ul> </ul>
</div> </div>
))} ))}
<div className="prose max-w-full md:prose-lg dark:prose-invert mb-6 md:mb-9"> <div className="prose max-w-full md:prose-lg dark:prose-invert mb-6 md:mb-9">
<ReactMarkdown>{VULNERABILITY}</ReactMarkdown> <ReactMarkdown>{VULNERABILITY}</ReactMarkdown>
</div> </div>
{/* Section with MODS content */} {/* Section with MODS content */}
{reversedMods.map((mod) => ( {reversedMods.map((mod) => (
<div <div
key={mod._id} key={mod._id}
className="flex-start flex-col my-6 md:my-9 gap-4 w-full" className="flex-start flex-col my-6 md:my-9 gap-4 w-full"
> >
<h2 className="text-2xl md:text-3xl py-1 md:py-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400 -mb-1"> <h2 className="text-2xl md:text-3xl py-1 md:py-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400 -mb-1">
{mod.title} {mod.title}
</h2> </h2>
{mod.vulnerabilities && ( {mod.vulnerabilities && (
<div className="prose max-w-full md:prose-lg dark:prose-invert "> <div className="prose max-w-full md:prose-lg dark:prose-invert ">
<ReactMarkdown>{mod.vulnerabilities}</ReactMarkdown> <ReactMarkdown>{mod.vulnerabilities}</ReactMarkdown>
</div> </div>
)} )}
</div> </div>
))} ))}
</section> </section>
); );
}; };
export default Vulnerabilities; export default Vulnerabilities;

View file

@ -9,7 +9,7 @@ export const authOptions: NextAuthOptions = {
name: "Credentials", name: "Credentials",
credentials: { credentials: {
username: { label: "Username", type: "text" }, username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" }, password: { label: "Password", type: "password" }
}, },
async authorize(credentials: any): Promise<any> { async authorize(credentials: any): Promise<any> {
const adminUsername = process.env.ADMIN_USERNAME; const adminUsername = process.env.ADMIN_USERNAME;
@ -34,8 +34,8 @@ export const authOptions: NextAuthOptions = {
} }
// If you return null then an error will be displayed that the user to check their details. // If you return null then an error will be displayed that the user to check their details.
return null; return null;
}, }
}), })
], ],
callbacks: { callbacks: {
async jwt({ token, user }) { async jwt({ token, user }) {
@ -51,13 +51,13 @@ export const authOptions: NextAuthOptions = {
// session.user.id = token.id; // session.user.id = token.id;
// session.user.name = token.name; // session.user.name = token.name;
return session; return session;
}, }
}, },
pages: { pages: {
signIn: "/login", signIn: "/login"
}, },
session: { session: {
strategy: "jwt", strategy: "jwt"
}, },
secret: process.env.NEXTAUTH_SECRET, secret: process.env.NEXTAUTH_SECRET
}; };

View file

@ -2,43 +2,43 @@ import { mailOptions, transporter } from "@/lib/nodemailer/nodemailer";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
const CONTACT_MESSAGE_FIELDS: Record<string, string> = { const CONTACT_MESSAGE_FIELDS: Record<string, string> = {
name: "Name", name: "Name",
email: "Email", email: "Email",
message: "Message", message: "Message"
}; };
const escapeHtml = (text: string) => { const escapeHtml = (text: string) => {
return text return text
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;") .replace(/>/g, "&gt;")
.replace(/"/g, "&quot;") .replace(/"/g, "&quot;")
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
}; };
const generateEmailContent = (data: Record<string, string>) => { const generateEmailContent = (data: Record<string, string>) => {
const stringData = Object.entries(data).reduce( const stringData = Object.entries(data).reduce(
(str, [key, val]) => (str, [key, val]) =>
str + str +
`${CONTACT_MESSAGE_FIELDS[key] || key}: ${val.replace(/\n/g, "\n")} \n\n`, `${CONTACT_MESSAGE_FIELDS[key] || key}: ${val.replace(/\n/g, "\n")} \n\n`,
"" ""
); );
const htmlData = Object.entries(data).reduce( const htmlData = Object.entries(data).reduce(
(str, [key, val]) => (str, [key, val]) =>
str + str +
`<h3 class="form-heading">${escapeHtml( `<h3 class="form-heading">${escapeHtml(
CONTACT_MESSAGE_FIELDS[key] || key CONTACT_MESSAGE_FIELDS[key] || key
)}</h3><p class="form-answer">${escapeHtml(val).replace( )}</h3><p class="form-answer">${escapeHtml(val).replace(
/\n/g, /\n/g,
"<br/>" "<br/>"
)}</p>`, )}</p>`,
"" ""
); );
return { return {
text: stringData, text: stringData,
html: `<!DOCTYPE html> html: `<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Contact Email</title> <title>Contact Email</title>
@ -90,37 +90,37 @@ const generateEmailContent = (data: Record<string, string>) => {
</tr> </tr>
</table> </table>
</body> </body>
</html>`, </html>`
}; };
}; };
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
if (req.method !== "POST") { if (req.method !== "POST") {
return NextResponse.json( return NextResponse.json(
{ message: "Method Not Allowed" }, { message: "Method Not Allowed" },
{ status: 405 } { status: 405 }
); );
} }
try { try {
const data = await req.json(); const data = await req.json();
console.log(data); console.log(data);
await transporter.sendMail({ await transporter.sendMail({
...mailOptions, ...mailOptions,
...generateEmailContent(data), ...generateEmailContent(data),
subject: "Contact Email", subject: "Contact Email"
}); });
return NextResponse.json( return NextResponse.json(
{ message: "Email sent successfully" }, { message: "Email sent successfully" },
{ status: 200 } { status: 200 }
); );
} catch (error) { } catch (error) {
console.error("Error sending email:", error); console.error("Error sending email:", error);
return NextResponse.json( return NextResponse.json(
{ message: "Internal Server Error" }, { message: "Internal Server Error" },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -4,27 +4,27 @@ import { ObjectId } from "mongodb";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function DELETE( export async function DELETE(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: { id: string } }
) { ) {
const { id } = params; const { id } = params;
try { try {
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const collection = db.collection("downloads"); const collection = db.collection("downloads");
const result = await collection.deleteOne({ _id: new ObjectId(id) }); const result = await collection.deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 1) { if (result.deletedCount === 1) {
return NextResponse.json({ message: "Log deleted successfully" }); return NextResponse.json({ message: "Log deleted successfully" });
} else { } else {
return NextResponse.json({ message: "Log not found" }, { status: 404 }); return NextResponse.json({ message: "Log not found" }, { status: 404 });
} }
} catch (error) { } catch (error) {
return NextResponse.json( return NextResponse.json(
{ message: "Failed to delete log", error: error }, { message: "Failed to delete log", error: error },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -4,27 +4,27 @@ import { ObjectId } from "mongodb";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function DELETE( export async function DELETE(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: { id: string } }
) { ) {
const { id } = params; const { id } = params;
try { try {
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const collection = db.collection("logs"); const collection = db.collection("logs");
const result = await collection.deleteOne({ _id: new ObjectId(id) }); const result = await collection.deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 1) { if (result.deletedCount === 1) {
return NextResponse.json({ message: "Log deleted successfully" }); return NextResponse.json({ message: "Log deleted successfully" });
} else { } else {
return NextResponse.json({ message: "Log not found" }, { status: 404 }); return NextResponse.json({ message: "Log not found" }, { status: 404 });
} }
} catch (error) { } catch (error) {
return NextResponse.json( return NextResponse.json(
{ message: "Failed to delete log", error: error }, { message: "Failed to delete log", error: error },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -4,27 +4,27 @@ import { ObjectId } from "mongodb";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function DELETE( export async function DELETE(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: { id: string } }
) { ) {
const { id } = params; const { id } = params;
try { try {
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const collection = db.collection("mods"); const collection = db.collection("mods");
const result = await collection.deleteOne({ _id: new ObjectId(id) }); const result = await collection.deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 1) { if (result.deletedCount === 1) {
return NextResponse.json({ message: "Log deleted successfully" }); return NextResponse.json({ message: "Log deleted successfully" });
} else { } else {
return NextResponse.json({ message: "Log not found" }, { status: 404 }); return NextResponse.json({ message: "Log not found" }, { status: 404 });
} }
} catch (error) { } catch (error) {
return NextResponse.json( return NextResponse.json(
{ message: "Failed to delete log", error: error }, { message: "Failed to delete log", error: error },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -3,36 +3,36 @@ import { ObjectId } from "mongodb";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function DELETE( export async function DELETE(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: { id: string } }
) { ) {
const { id } = params; const { id } = params;
if (!id) { if (!id) {
return NextResponse.json({ message: "ID is required" }, { status: 400 }); return NextResponse.json({ message: "ID is required" }, { status: 400 });
} }
try { try {
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const result = await db const result = await db
.collection("vulnerabilities") .collection("vulnerabilities")
.deleteOne({ _id: new ObjectId(id) }); .deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 1) { if (result.deletedCount === 1) {
return NextResponse.json( return NextResponse.json(
{ message: "Vulnerability deleted successfully" }, { message: "Vulnerability deleted successfully" },
{ status: 200 } { status: 200 }
); );
} else { } else {
return NextResponse.json( return NextResponse.json(
{ message: "Vulnerability not found" }, { message: "Vulnerability not found" },
{ status: 404 } { status: 404 }
); );
} }
} catch (error) { } catch (error) {
return NextResponse.json( return NextResponse.json(
{ message: "Failed to delete vulnerability" }, { message: "Failed to delete vulnerability" },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -5,30 +5,30 @@ import { serialize } from "cookie";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const { username, password } = await request.json(); const { username, password } = await request.json();
const adminUsername = process.env.ADMIN_USERNAME; const adminUsername = process.env.ADMIN_USERNAME;
const adminPassword = process.env.ADMIN_PASSWORD; const adminPassword = process.env.ADMIN_PASSWORD;
if (username === adminUsername && password === adminPassword) { if (username === adminUsername && password === adminPassword) {
const cookie = serialize("auth", "authenticated", { const cookie = serialize("auth", "authenticated", {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
maxAge: 60 * 60 * 24, // 1 day maxAge: 60 * 60 * 24 // 1 day
}); });
return new NextResponse(JSON.stringify({ message: "Login successful" }), { return new NextResponse(JSON.stringify({ message: "Login successful" }), {
headers: { headers: {
"Set-Cookie": cookie, "Set-Cookie": cookie,
"Content-Type": "application/json", "Content-Type": "application/json"
}, }
}); });
} }
return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), { return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), {
status: 401, status: 401,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, }
}); });
} }

View file

@ -6,15 +6,15 @@ export const dynamic = "force-dynamic";
// Handler for GET requests // Handler for GET requests
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
try { try {
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const downloads = await db.collection("logs").find().toArray(); const downloads = await db.collection("logs").find().toArray();
return NextResponse.json(downloads, { status: 200 }); return NextResponse.json(downloads, { status: 200 });
} catch (error) { } catch (error) {
return NextResponse.json( return NextResponse.json(
{ error: "Failed to fetch logs" }, { error: "Failed to fetch logs" },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -2,104 +2,104 @@ import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/lib/db"; import clientPromise from "@/lib/db";
export const GET = async ( export const GET = async (
req: NextRequest, req: NextRequest,
{ params }: { params: { slug: string } } { params }: { params: { slug: string } }
) => { ) => {
const client = await clientPromise; const client = await clientPromise;
const db = client.db(); const db = client.db();
const { slug } = params; const { slug } = params;
if (!slug) { if (!slug) {
return NextResponse.json({ message: "Slug is required" }, { status: 400 }); return NextResponse.json({ message: "Slug is required" }, { status: 400 });
} }
const page = await db.collection("pages").findOne({ slug }); const page = await db.collection("pages").findOne({ slug });
if (page) { if (page) {
return NextResponse.json(page, { status: 200 }); return NextResponse.json(page, { status: 200 });
} else { } else {
return NextResponse.json({ message: "Page not found" }, { status: 404 }); return NextResponse.json({ message: "Page not found" }, { status: 404 });
} }
}; };
export const PUT = async ( export const PUT = async (
req: NextRequest, req: NextRequest,
{ params }: { params: { slug: string } } { params }: { params: { slug: string } }
) => { ) => {
const client = await clientPromise; const client = await clientPromise;
const db = client.db(); const db = client.db();
const { slug } = params; const { slug } = params;
if (!slug) { if (!slug) {
return NextResponse.json({ message: "Slug is required" }, { status: 400 }); return NextResponse.json({ message: "Slug is required" }, { status: 400 });
} }
const { title, content, vulnerabilities } = await req.json(); const { title, content, vulnerabilities } = await req.json();
if ( if (
typeof title !== "string" || typeof title !== "string" ||
typeof content !== "string" || typeof content !== "string" ||
typeof vulnerabilities !== "string" typeof vulnerabilities !== "string"
) { ) {
return NextResponse.json( return NextResponse.json(
{ message: "Invalid title, content, or vulnerabilities" }, { message: "Invalid title, content, or vulnerabilities" },
{ status: 400 } { status: 400 }
); );
} }
try { try {
const result = await db.collection("pages").findOneAndUpdate( const result = await db.collection("pages").findOneAndUpdate(
{ slug }, { slug },
{ $set: { title, content, vulnerabilities } }, { $set: { title, content, vulnerabilities } },
{ returnDocument: "after" } // Updated option { returnDocument: "after" } // Updated option
); );
if (result?.value) { if (result?.value) {
const serializedResult = { const serializedResult = {
...result.value, ...result.value,
_id: result.value._id.toString(), // Convert ObjectId to string _id: result.value._id.toString() // Convert ObjectId to string
}; };
return NextResponse.json(serializedResult, { status: 200 }); return NextResponse.json(serializedResult, { status: 200 });
} else { } else {
return NextResponse.json({ message: "Page not found" }, { status: 404 }); return NextResponse.json({ message: "Page not found" }, { status: 404 });
} }
} catch (error) { } catch (error) {
console.error("Error updating page:", error); console.error("Error updating page:", error);
return NextResponse.json( return NextResponse.json(
{ message: "Failed to update page" }, { message: "Failed to update page" },
{ status: 500 } { status: 500 }
); );
} }
}; };
export const DELETE = async ( export const DELETE = async (
req: NextRequest, req: NextRequest,
{ params }: { params: { slug: string } } { params }: { params: { slug: string } }
) => { ) => {
const client = await clientPromise; const client = await clientPromise;
const db = client.db(); const db = client.db();
const { slug } = params; const { slug } = params;
if (!slug) { if (!slug) {
return NextResponse.json({ message: "Slug is required" }, { status: 400 }); return NextResponse.json({ message: "Slug is required" }, { status: 400 });
} }
try { try {
const result = await db.collection("pages").deleteOne({ slug }); const result = await db.collection("pages").deleteOne({ slug });
if (result.deletedCount > 0) { if (result.deletedCount > 0) {
return NextResponse.json( return NextResponse.json(
{ message: "Page deleted successfully" }, { message: "Page deleted successfully" },
{ status: 200 } { status: 200 }
); );
} else { } else {
return NextResponse.json({ message: "Page not found" }, { status: 404 }); return NextResponse.json({ message: "Page not found" }, { status: 404 });
} }
} catch (error) { } catch (error) {
console.error("Error deleting page:", error); console.error("Error deleting page:", error);
return NextResponse.json( return NextResponse.json(
{ message: "Failed to delete page" }, { message: "Failed to delete page" },
{ status: 500 } { status: 500 }
); );
} }
}; };

View file

@ -2,42 +2,42 @@ import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/lib/db"; import clientPromise from "@/lib/db";
export const GET = async (req: NextRequest) => { export const GET = async (req: NextRequest) => {
const client = await clientPromise; const client = await clientPromise;
const db = client.db(); const db = client.db();
try { try {
const pages = await db.collection("pages").find().toArray(); const pages = await db.collection("pages").find().toArray();
return NextResponse.json(pages, { status: 200 }); return NextResponse.json(pages, { status: 200 });
} catch (error) { } catch (error) {
console.error("Error fetching pages:", error); console.error("Error fetching pages:", error);
return NextResponse.json( return NextResponse.json(
{ message: "Failed to fetch pages" }, { message: "Failed to fetch pages" },
{ status: 500 } { status: 500 }
); );
} }
}; };
export const POST = async (req: NextRequest) => { export const POST = async (req: NextRequest) => {
const client = await clientPromise; const client = await clientPromise;
const db = client.db(); const db = client.db();
const { title, slug, content } = await req.json(); const { title, slug, content } = await req.json();
if (!slug) { if (!slug) {
return NextResponse.json( return NextResponse.json(
{ message: "Missing required fields" }, { message: "Missing required fields" },
{ status: 400 } { status: 400 }
); );
} }
try { try {
const newPage = { title, slug, content }; const newPage = { title, slug, content };
const result = await db.collection("pages").insertOne(newPage); const result = await db.collection("pages").insertOne(newPage);
return NextResponse.json(newPage, { status: 201 }); return NextResponse.json(newPage, { status: 201 });
} catch (error) { } catch (error) {
console.error("Error creating page:", error); console.error("Error creating page:", error);
return NextResponse.json( return NextResponse.json(
{ message: "Failed to create page" }, { message: "Failed to create page" },
{ status: 500 } { status: 500 }
); );
} }
}; };

View file

@ -3,63 +3,63 @@ import nodemailer from "nodemailer";
import clientPromise from "@/lib/db"; import clientPromise from "@/lib/db";
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: "smtp.ethereal.email", //replace this also comment this if u not using etheral host: "smtp.ethereal.email", //replace this also comment this if u not using etheral
// service: "gmail", // uncomment if u using gmail // service: "gmail", // uncomment if u using gmail
port: 587, port: 587,
auth: { auth: {
user: process.env.EMAIL, user: process.env.EMAIL,
pass: process.env.EMAIL_PASS, pass: process.env.EMAIL_PASS
}, }
}); });
const sendEmail = async (to: string[], subject: string, html: string) => { const sendEmail = async (to: string[], subject: string, html: string) => {
try { try {
await transporter.sendMail({ await transporter.sendMail({
from: process.env.EMAIL_USER, from: process.env.EMAIL_USER,
to: to.join(", "), to: to.join(", "),
subject: subject, subject: subject,
html: html, html: html
}); });
} catch (error) { } catch (error) {
console.error("Error sending email:", error); console.error("Error sending email:", error);
throw new Error("Failed to send email"); throw new Error("Failed to send email");
} }
}; };
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const { subject, html } = await req.json(); const { subject, html } = await req.json();
const client = await clientPromise; const client = await clientPromise;
const db = client.db("newsletter"); const db = client.db("newsletter");
const collection = db.collection("subscribers"); const collection = db.collection("subscribers");
const subscribers = await collection const subscribers = await collection
.find({}, { projection: { email: 1 } }) .find({}, { projection: { email: 1 } })
.toArray(); .toArray();
if (subscribers.length === 0) { if (subscribers.length === 0) {
console.error("No subscribers found in the database."); console.error("No subscribers found in the database.");
return NextResponse.json( return NextResponse.json(
{ message: "No subscribers found." }, { message: "No subscribers found." },
{ status: 404 } { status: 404 }
); );
} }
const emails = subscribers.map((subscriber) => subscriber.email); const emails = subscribers.map((subscriber) => subscriber.email);
if (emails.length === 0) { if (emails.length === 0) {
console.error("No email addresses found."); console.error("No email addresses found.");
return NextResponse.json( return NextResponse.json(
{ message: "No email addresses found." }, { message: "No email addresses found." },
{ status: 404 } { status: 404 }
); );
} }
await sendEmail(emails, subject, html); await sendEmail(emails, subject, html);
return NextResponse.json({ message: "Emails sent successfully" }); return NextResponse.json({ message: "Emails sent successfully" });
} catch (error) { } catch (error) {
console.error("Error handling POST request:", error); console.error("Error handling POST request:", error);
return NextResponse.error(); return NextResponse.error();
} }
} }

View file

@ -2,43 +2,43 @@ import { NextResponse } from "next/server";
import clientPromise from "@/lib/db"; import clientPromise from "@/lib/db";
interface Subscriber { interface Subscriber {
email: string; email: string;
subscribedAt: Date; subscribedAt: Date;
} }
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function GET(req: Request) { export async function GET(req: Request) {
try { try {
const url = new URL(req.url); const url = new URL(req.url);
const page = parseInt(url.searchParams.get("page") || "1", 10); const page = parseInt(url.searchParams.get("page") || "1", 10);
const limit = 10; const limit = 10;
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const client = await clientPromise; const client = await clientPromise;
const db = client.db("newsletter"); const db = client.db("newsletter");
const collection = db.collection("subscribers"); const collection = db.collection("subscribers");
// Pagination // Pagination
const documents = await collection.find().skip(skip).limit(limit).toArray(); const documents = await collection.find().skip(skip).limit(limit).toArray();
const subscribers: Subscriber[] = documents.map((doc) => ({ const subscribers: Subscriber[] = documents.map((doc) => ({
email: doc.email, email: doc.email,
subscribedAt: subscribedAt:
doc.subscribedAt instanceof Date doc.subscribedAt instanceof Date
? doc.subscribedAt ? doc.subscribedAt
: new Date(doc.subscribedAt), : new Date(doc.subscribedAt)
})); }));
const totalSubscribers = await collection.countDocuments(); const totalSubscribers = await collection.countDocuments();
return NextResponse.json({ return NextResponse.json({
subscribers, subscribers,
totalSubscribers, totalSubscribers,
totalPages: Math.ceil(totalSubscribers / limit), totalPages: Math.ceil(totalSubscribers / limit)
}); });
} catch (error) { } catch (error) {
console.error("Error fetching subscribers:", error); console.error("Error fetching subscribers:", error);
return new NextResponse("Internal Server Error", { status: 500 }); return new NextResponse("Internal Server Error", { status: 500 });
} }
} }

View file

@ -2,51 +2,51 @@ import { NextRequest, NextResponse } from "next/server";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: "smtp.ethereal.email", // Replace with your SMTP host host: "smtp.ethereal.email", // Replace with your SMTP host
// service: "gmail", // Uncomment if using Gmail // service: "gmail", // Uncomment if using Gmail
port: 587, port: 587,
auth: { auth: {
user: process.env.EMAIL, user: process.env.EMAIL,
pass: process.env.EMAIL_PASS, pass: process.env.EMAIL_PASS
}, }
}); });
const sendEmail = async (to: string[], subject: string, html: string) => { const sendEmail = async (to: string[], subject: string, html: string) => {
try { try {
await transporter.sendMail({ await transporter.sendMail({
from: process.env.EMAIL_USER, from: process.env.EMAIL_USER,
to: to.join(", "), to: to.join(", "),
subject: subject, subject: subject,
html: html, html: html
}); });
} catch (error) { } catch (error) {
console.error("Error sending email:", error); console.error("Error sending email:", error);
throw new Error("Failed to send email"); throw new Error("Failed to send email");
} }
}; };
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const { subject, html } = await req.json(); const { subject, html } = await req.json();
// add ur email here // add ur email here
const testEmails = [ const testEmails = [
"abhijitbhattacharjee333@gmail.com", "abhijitbhattacharjee333@gmail.com",
"test2@example.com", "test2@example.com"
]; ];
if (testEmails.length === 0) { if (testEmails.length === 0) {
console.error("No email addresses provided."); console.error("No email addresses provided.");
return NextResponse.json( return NextResponse.json(
{ message: "No email addresses provided." }, { message: "No email addresses provided." },
{ status: 404 } { status: 404 }
); );
} }
await sendEmail(testEmails, subject, html); await sendEmail(testEmails, subject, html);
return NextResponse.json({ message: "Emails sent successfully" }); return NextResponse.json({ message: "Emails sent successfully" });
} catch (error) { } catch (error) {
console.error("Error handling POST request:", error); console.error("Error handling POST request:", error);
return NextResponse.error(); return NextResponse.error();
} }
} }

View file

@ -2,62 +2,62 @@ import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/lib/db"; import clientPromise from "@/lib/db";
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const { email, captchaToken } = await req.json(); const { email, captchaToken } = await req.json();
if (!email || !captchaToken) { if (!email || !captchaToken) {
return NextResponse.json( return NextResponse.json(
{ message: "Email and captcha token are required." }, { message: "Email and captcha token are required." },
{ status: 400 } { status: 400 }
); );
} }
// Verify hCaptcha token // Verify hCaptcha token
const hcaptchaResponse = await fetch( const hcaptchaResponse = await fetch(
`https://api.hcaptcha.com/siteverify`, `https://api.hcaptcha.com/siteverify`,
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded"
}, },
body: `secret=${process.env.HCAPTCHA_SECRET}&response=${captchaToken}`, body: `secret=${process.env.HCAPTCHA_SECRET}&response=${captchaToken}`
} }
); );
const hcaptchaData = await hcaptchaResponse.json(); const hcaptchaData = await hcaptchaResponse.json();
if (!hcaptchaData.success) { if (!hcaptchaData.success) {
return NextResponse.json( return NextResponse.json(
{ message: "Captcha verification failed." }, { message: "Captcha verification failed." },
{ status: 400 } { status: 400 }
); );
} }
const client = await clientPromise; const client = await clientPromise;
const db = client.db("newsletter"); const db = client.db("newsletter");
const collection = db.collection("subscribers"); const collection = db.collection("subscribers");
// checking if email alr exists // checking if email alr exists
const existingSubscriber = await collection.findOne({ email }); const existingSubscriber = await collection.findOne({ email });
if (existingSubscriber) { if (existingSubscriber) {
return NextResponse.json( return NextResponse.json(
{ message: "This email is already subscribed." }, { message: "This email is already subscribed." },
{ status: 409 } { status: 409 }
); );
} }
// saves the email in the db // saves the email in the db
await collection.insertOne({ email, subscribedAt: new Date() }); await collection.insertOne({ email, subscribedAt: new Date() });
return NextResponse.json( return NextResponse.json(
{ message: "Successfully subscribed!" }, { message: "Successfully subscribed!" },
{ status: 201 } { status: 201 }
); );
} catch (error) { } catch (error) {
console.error("Error subscribing:", error); console.error("Error subscribing:", error);
return NextResponse.json( return NextResponse.json(
{ message: "Internal Server Error" }, { message: "Internal Server Error" },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -5,42 +5,42 @@ import { ObjectId } from "mongodb";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function PUT( export async function PUT(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: { id: string } }
) { ) {
const { id } = params; const { id } = params;
const body = await request.json(); const body = await request.json();
const { fileName, version, downloadLink, fileSize } = body; const { fileName, version, downloadLink, fileSize } = body;
try { try {
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const result = await db.collection("mods").updateOne( const result = await db.collection("mods").updateOne(
{ _id: new ObjectId(id) }, { _id: new ObjectId(id) },
{ {
$set: { $set: {
fileName, fileName,
version, version,
downloadLink, downloadLink,
fileSize, fileSize
}, }
} }
); );
if (result.modifiedCount > 0) { if (result.modifiedCount > 0) {
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} else { } else {
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
message: "No document updated", message: "No document updated"
}); });
} }
} catch (error) { } catch (error) {
console.error("Update failed", error); console.error("Update failed", error);
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
message: "Failed to update mod", message: "Failed to update mod"
}); });
} }
} }

View file

@ -16,7 +16,7 @@ export async function POST(request: Request) {
fileName, fileName,
version, version,
downloadLink, downloadLink,
fileSize, fileSize
}); });
return NextResponse.json({ success: true, id: result.insertedId }); return NextResponse.json({ success: true, id: result.insertedId });

View file

@ -5,17 +5,17 @@ import clientPromise from "@/lib/db";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function POST(request: Request) { export async function POST(request: Request) {
const body = await request.json(); const body = await request.json();
const { version, date, bullets } = body; const { version, date, bullets } = body;
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const result = await db.collection("logs").insertOne({ const result = await db.collection("logs").insertOne({
version, version,
date, date,
bullets, bullets
}); });
return NextResponse.json({ success: true, id: result.insertedId }); return NextResponse.json({ success: true, id: result.insertedId });
} }

View file

@ -16,7 +16,7 @@ export async function POST(request: Request) {
fileName, fileName,
version, version,
downloadLink, downloadLink,
fileSize, fileSize
}); });
return NextResponse.json({ success: true, id: result.insertedId }); return NextResponse.json({ success: true, id: result.insertedId });

View file

@ -6,11 +6,11 @@ const f = createUploadthing();
// const auth = (req: Request) => ({ id: "fakeId" }); // const auth = (req: Request) => ({ id: "fakeId" });
export const ourFileRouter = { export const ourFileRouter = {
imageUploader: f({ imageUploader: f({
"application/zip": { maxFileSize: "8MB" }, "application/zip": { maxFileSize: "8MB" }
}).onUploadComplete(async ({ metadata, file }) => { }).onUploadComplete(async ({ metadata, file }) => {
console.log("file url", file.url); console.log("file url", file.url);
}), })
} satisfies FileRouter; } satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter; export type OurFileRouter = typeof ourFileRouter;

View file

@ -4,5 +4,5 @@ import { ourFileRouter } from "./core";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const { GET, POST } = createRouteHandler({ export const { GET, POST } = createRouteHandler({
router: ourFileRouter, router: ourFileRouter
}); });

View file

@ -5,16 +5,16 @@ import clientPromise from "@/lib/db";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function POST(request: Request) { export async function POST(request: Request) {
const body = await request.json(); const body = await request.json();
const { version, date, bullets } = body; const { version, date, bullets } = body;
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const result = await db.collection("vulnerabilities").insertOne({ const result = await db.collection("vulnerabilities").insertOne({
version, version,
bullets, bullets
}); });
return NextResponse.json({ success: true, id: result.insertedId }); return NextResponse.json({ success: true, id: result.insertedId });
} }

View file

@ -6,15 +6,15 @@ export const dynamic = "force-dynamic";
// Handler for GET requests // Handler for GET requests
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
try { try {
const client = await clientPromise; const client = await clientPromise;
const db = client.db("downloadsDatabase"); const db = client.db("downloadsDatabase");
const downloads = await db.collection("vulnerabilities").find().toArray(); const downloads = await db.collection("vulnerabilities").find().toArray();
return NextResponse.json(downloads, { status: 200 }); return NextResponse.json(downloads, { status: 200 });
} catch (error) { } catch (error) {
return NextResponse.json( return NextResponse.json(
{ error: "Failed to fetch logs" }, { error: "Failed to fetch logs" },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -6,60 +6,59 @@ import { Toaster } from "@/components/ui/toaster";
import { Analytics } from "@vercel/analytics/react"; import { Analytics } from "@vercel/analytics/react";
const poppins = Poppins({ const poppins = Poppins({
weight: ["400", "600", "700", "900"], weight: ["400", "600", "700", "900"],
subsets: ["latin"], subsets: ["latin"]
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "SVRJS - A Web Server running on Node.js", title: "SVRJS - A Web Server running on Node.js",
description: 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).", "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: { openGraph: {
title: "SVRJS - A Web Server running on Node.js", title: "SVRJS - A Web Server running on Node.js",
description: 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).", "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", url: "https://svrjs.org",
type: "website", type: "website",
images: [ images: [
{ {
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png", url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800, width: 800,
height: 600, height: 600,
alt: "SVRJS - A Web Server running on Node.js", alt: "SVRJS - A Web Server running on Node.js"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@SVR_JS", site: "@SVR_JS",
title: "SVRJS - A Web Server running on Node.js", title: "SVRJS - A Web Server running on Node.js",
description: 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).", "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"], images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS", creator: "@SVR_JS"
}, }
}; };
export default function RootLayout({ export default function RootLayout({
children, children
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`antialiased ${poppins.className}`}> <body className={`antialiased ${poppins.className}`}>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="dark" defaultTheme="dark"
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >
{children}
{children} <Toaster />
<Toaster /> <Analytics />
<Analytics /> </ThemeProvider>
</ThemeProvider> </body>
</body> </html>
</html> );
);
} }

View file

@ -30,7 +30,7 @@ const LoginPage = () => {
const res = await signIn("credentials", { const res = await signIn("credentials", {
redirect: false, redirect: false,
username, username,
password, password
}); });
if (res?.ok) { if (res?.ok) {

View file

@ -3,28 +3,28 @@ import Navbar from "@/components/shared/Navbar";
import Link from "next/link"; import Link from "next/link";
const NotFound = () => { const NotFound = () => {
return ( return (
<> <>
<main className="flex flex-col min-h-screen"> <main className="flex flex-col min-h-screen">
<Navbar /> <Navbar />
<section <section
id="404error" id="404error"
className="flex-center flex-col wrapper container flex-1 flex-grow" className="flex-center flex-col wrapper container flex-1 flex-grow"
> >
<h1 className="text-3xl md:text-5xl text-center"> <h1 className="text-3xl md:text-5xl text-center">
<span className="text-red-500">404</span> Page not Found <span className="text-red-500">404</span> Page not Found
</h1> </h1>
<p className="text-lg mt-3 text-muted-foreground"> <p className="text-lg mt-3 text-muted-foreground">
Please return back to{" "} Please return back to{" "}
<Link href="/" className="underline font-bold"> <Link href="/" className="underline font-bold">
Home Home
</Link> </Link>
</p> </p>
</section> </section>
<Footer /> <Footer />
</main> </main>
</> </>
); );
}; };
export default NotFound; export default NotFound;

View file

@ -4,13 +4,13 @@ import { client } from "@/lib/sanity";
import { toHTML } from "@portabletext/to-html"; import { toHTML } from "@portabletext/to-html";
export async function GET() { export async function GET() {
// Define the site URL based on the environment // Define the site URL based on the environment
const SITE_URL = const SITE_URL =
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
? "https://svrjs.vercel.app" ? "https://svrjs.vercel.app"
: "http://localhost:3000"; : "http://localhost:3000";
const postsQuery = `*[_type == 'blog'] | order(_createdAt desc) { const postsQuery = `*[_type == 'blog'] | order(_createdAt desc) {
title, title,
"slug": slug.current, "slug": slug.current,
content, content,
@ -18,33 +18,33 @@ export async function GET() {
_createdAt _createdAt
}`; }`;
const posts = await client.fetch(postsQuery); const posts = await client.fetch(postsQuery);
const feed = new RSS({ const feed = new RSS({
title: "SVRJS Blog", title: "SVRJS Blog",
description: "Explore the latest blog posts from SVRJS", description: "Explore the latest blog posts from SVRJS",
feed_url: `${SITE_URL}/rss.xml`, feed_url: `${SITE_URL}/rss.xml`,
site_url: `${SITE_URL}`, site_url: `${SITE_URL}`,
image_url: `${SITE_URL}/metadata/svrjs-cover.png`, image_url: `${SITE_URL}/metadata/svrjs-cover.png`,
language: "en-US", language: "en-US",
pubDate: new Date().toUTCString(), pubDate: new Date().toUTCString()
}); });
posts.forEach((post: any) => { posts.forEach((post: any) => {
feed.item({ feed.item({
title: post.title, title: post.title,
description: toHTML(post.content), description: toHTML(post.content),
url: `${SITE_URL}/blog/${post.slug}`, url: `${SITE_URL}/blog/${post.slug}`,
date: new Date(post._createdAt).toUTCString(), date: new Date(post._createdAt).toUTCString()
// uncomment this if u want to // uncomment this if u want to
// enclosure: { url: urlFor(post.titleImage).url() }, // enclosure: { url: urlFor(post.titleImage).url() },
// author: "SVRJS", // author: "SVRJS",
}); });
}); });
return new NextResponse(feed.xml({ indent: true }), { return new NextResponse(feed.xml({ indent: true }), {
headers: { headers: {
"Content-Type": "application/xml", "Content-Type": "application/xml"
}, }
}); });
} }

View file

@ -1,30 +1,30 @@
import { getAllBlogPostSlugs } from "@/lib/getBlogPost"; import { getAllBlogPostSlugs } from "@/lib/getBlogPost";
export default async function sitemap() { export default async function sitemap() {
const blogPostSlugs = await getAllBlogPostSlugs(); const blogPostSlugs = await getAllBlogPostSlugs();
const baseRoutes = [ const baseRoutes = [
"/", "/",
"/blog", "/blog",
"/changelogs", "/changelogs",
"/contact", "/contact",
"/contribute", "/contribute",
"/downloads", "/downloads",
"/forum", "/forum",
"/mods", "/mods",
"/privacy-policy", "/privacy-policy",
"/tos", "/tos",
"/vulnerabilities", "/vulnerabilities",
"/newsletter", "/newsletter"
].map((route) => ({ ].map((route) => ({
url: `https://svrjs.vercel.app${route}`, url: `https://svrjs.vercel.app${route}`,
lastModified: new Date().toISOString().split("T")[0], lastModified: new Date().toISOString().split("T")[0]
})); }));
const blogRoutes = blogPostSlugs.map((slug) => ({ const blogRoutes = blogPostSlugs.map((slug) => ({
url: `https://svrjs.vercel.app/blog/${slug.slug}`, url: `https://svrjs.vercel.app/blog/${slug.slug}`,
lastModified: new Date().toISOString().split("T")[0], lastModified: new Date().toISOString().split("T")[0]
})); }));
return [...baseRoutes, ...blogRoutes]; return [...baseRoutes, ...blogRoutes];
} }

View file

@ -5,32 +5,32 @@ import { ExternalLink } from "lucide-react";
import { client, urlFor } from "@/lib/sanity"; import { client, urlFor } from "@/lib/sanity";
import { Card, CardContent } from "../ui/card"; import { Card, CardContent } from "../ui/card";
import { import {
Pagination, Pagination,
PaginationContent, PaginationContent,
PaginationItem, PaginationItem,
PaginationLink, PaginationLink,
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import { format } from "date-fns"; import { format } from "date-fns";
interface BlogPostcard { interface BlogPostcard {
title: string; title: string;
smallDescription: string; smallDescription: string;
currentSlug: string; currentSlug: string;
titleImage: string; titleImage: string;
_createdAt: string; _createdAt: string;
} }
interface BlogCardsProps { interface BlogCardsProps {
searchParams: { page?: string }; searchParams: { page?: string };
} }
const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => { const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
const cardsPerPage = 6; const cardsPerPage = 6;
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1; const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
const query = `*[_type == 'blog'] | order(_createdAt desc) { const query = `*[_type == 'blog'] | order(_createdAt desc) {
title, title,
smallDescription, smallDescription,
"currentSlug": slug.current, "currentSlug": slug.current,
@ -38,94 +38,94 @@ const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
_createdAt _createdAt
}[${(currentPage - 1) * cardsPerPage}...${currentPage * cardsPerPage}]`; }[${(currentPage - 1) * cardsPerPage}...${currentPage * cardsPerPage}]`;
const posts: BlogPostcard[] = await client.fetch(query); const posts: BlogPostcard[] = await client.fetch(query);
const totalPostsQuery = `count(*[_type == 'blog'])`; const totalPostsQuery = `count(*[_type == 'blog'])`;
const totalPosts: number = await client.fetch(totalPostsQuery); const totalPosts: number = await client.fetch(totalPostsQuery);
const totalPages = Math.ceil(totalPosts / cardsPerPage); const totalPages = Math.ceil(totalPosts / cardsPerPage);
return ( return (
<> <>
<section className="grid max-w-6xl gap-4 mx-auto sm:grid-cols-2 lg:grid-cols-3"> <section className="grid max-w-6xl gap-4 mx-auto sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post, idx) => { {posts.map((post, idx) => {
const formattedDate = format( const formattedDate = format(
new Date(post._createdAt), new Date(post._createdAt),
"MMMM d, yyyy" "MMMM d, yyyy"
); );
const truncatedDescription = const truncatedDescription =
post.smallDescription.length > 130 post.smallDescription.length > 130
? post.smallDescription.substring(0, 130) + "..." ? post.smallDescription.substring(0, 130) + "..."
: post.smallDescription; : post.smallDescription;
return ( return (
<Card <Card
className="group h-full w-full rounded-lg border overflow-hidden" className="group h-full w-full rounded-lg border overflow-hidden"
key={idx} key={idx}
> >
<Link href={`/blog/${post.currentSlug}`} className="block"> <Link href={`/blog/${post.currentSlug}`} className="block">
<div className="relative overflow-hidden rounded-t-lg"> <div className="relative overflow-hidden rounded-t-lg">
<Image <Image
src={urlFor(post.titleImage).url()} src={urlFor(post.titleImage).url()}
alt={post.title} alt={post.title}
width={500} width={500}
height={300} height={300}
priority priority
className="w-full object-cover transition-transform duration-200 group-hover:scale-105" className="w-full object-cover transition-transform duration-200 group-hover:scale-105"
/> />
</div> </div>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex-between mb-2 py-2"> <div className="flex-between mb-2 py-2">
<h3 className="text-xl font-semibold leading-tight"> <h3 className="text-xl font-semibold leading-tight">
{post.title} {post.title}
</h3> </h3>
<div className="text-sm text-muted-foreground opacity-0 group-hover:opacity-100 duration-300"> <div className="text-sm text-muted-foreground opacity-0 group-hover:opacity-100 duration-300">
<ExternalLink /> <ExternalLink />
</div> </div>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{truncatedDescription} {truncatedDescription}
</p> </p>
<p className="text-xs text-muted-foreground mt-2"> <p className="text-xs text-muted-foreground mt-2">
Published on: {formattedDate} Published on: {formattedDate}
</p> </p>
</CardContent> </CardContent>
</Link> </Link>
</Card> </Card>
); );
})} })}
</section> </section>
<div className="flex-center mt-12"> <div className="flex-center mt-12">
{totalPages > 1 && ( {totalPages > 1 && (
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
{currentPage > 1 && ( {currentPage > 1 && (
<PaginationPrevious href={`?page=${currentPage - 1}`} /> <PaginationPrevious href={`?page=${currentPage - 1}`} />
)} )}
</PaginationItem> </PaginationItem>
{Array.from({ length: totalPages }).map((_, i) => ( {Array.from({ length: totalPages }).map((_, i) => (
<PaginationItem key={i}> <PaginationItem key={i}>
<PaginationLink <PaginationLink
href={`?page=${i + 1}`} href={`?page=${i + 1}`}
isActive={currentPage === i + 1} isActive={currentPage === i + 1}
> >
{i + 1} {i + 1}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
))} ))}
<PaginationItem> <PaginationItem>
{currentPage < totalPages && ( {currentPage < totalPages && (
<PaginationNext href={`?page=${currentPage + 1}`} /> <PaginationNext href={`?page=${currentPage + 1}`} />
)} )}
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination> </Pagination>
)} )}
</div> </div>
</> </>
); );
}; };
export default BlogCards; export default BlogCards;

View file

@ -4,27 +4,27 @@ import { Editor } from "@monaco-editor/react";
import { EXAMPLE_A1 } from "@/constants"; import { EXAMPLE_A1 } from "@/constants";
interface CodeEditorProps { interface CodeEditorProps {
onChange: (value: string) => void; onChange: (value: string) => void;
} }
const CodeEditor = ({ onChange }: CodeEditorProps) => { const CodeEditor = ({ onChange }: CodeEditorProps) => {
return ( return (
<div className="bg-white w-full max-w-full"> <div className="bg-white w-full max-w-full">
<Editor <Editor
options={{ options={{
minimap: { minimap: {
enabled: false, enabled: false
}, }
}} }}
height="75vh" height="75vh"
theme="vs-dark" theme="vs-dark"
defaultValue={EXAMPLE_A1} defaultValue={EXAMPLE_A1}
language={"html"} language={"html"}
onChange={(newValue) => onChange(newValue || "")} onChange={(newValue) => onChange(newValue || "")}
className="bg-zinc-950 text-white" className="bg-zinc-950 text-white"
/> />
</div> </div>
); );
}; };
export default CodeEditor; export default CodeEditor;

View file

@ -2,54 +2,54 @@ import Image from "next/image";
import React from "react"; import React from "react";
interface TestimonialCard { interface TestimonialCard {
avatar: string; avatar: string;
name: string; name: string;
role?: string; role?: string;
testimonial: string; testimonial: string;
rating: number; rating: number;
} }
const TestimonialCard = ({ const TestimonialCard = ({
avatar, avatar,
name, name,
role, role,
testimonial, testimonial,
rating, rating
}: TestimonialCard) => { }: TestimonialCard) => {
return ( return (
<li className="inline-block w-full"> <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="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 className="flex flex-row items-center gap-3">
<div> <div>
<Image <Image
src={`/testimonials/${avatar}.webp`} src={`/testimonials/${avatar}.webp`}
alt="avatar1" alt="avatar1"
width={40} width={40}
height={40} height={40}
className="rounded-full" className="rounded-full"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="small-semibold dark:text-white">{name}</div> <div className="small-semibold dark:text-white">{name}</div>
</div> </div>
<div className="text-sm text-muted-foreground">{role}</div> <div className="text-sm text-muted-foreground">{role}</div>
</div> </div>
</div> </div>
<p className="body-regular dark:text-white">{testimonial}</p> <p className="body-regular dark:text-white">{testimonial}</p>
<div className="hue-rotate-90 text-lg"> <div className="hue-rotate-90 text-lg">
{/* <Image {/* <Image
src="/testimonials/stars.svg" src="/testimonials/stars.svg"
alt="star" alt="star"
width={120} width={120}
height={120} height={120}
className="object-cover" className="object-cover"
/> */} /> */}
{"⭐".repeat(rating)} {"⭐".repeat(rating)}
</div> </div>
</div> </div>
</li> </li>
); );
}; };
export default TestimonialCard; export default TestimonialCard;

View file

@ -13,11 +13,11 @@ import "prismjs/components/prism-markup-templating";
import "prismjs/components/prism-handlebars"; import "prismjs/components/prism-handlebars";
export default function PrismLoader() { export default function PrismLoader() {
useEffect(() => { useEffect(() => {
if (Prism) { if (Prism) {
Prism.highlightAll(); Prism.highlightAll();
} }
}, []); }, []);
return null; return null;
} }

View file

@ -3,40 +3,40 @@ import React from "react";
import Statistics from "./Statistics"; import Statistics from "./Statistics";
const About = () => { const About = () => {
return ( return (
<section id="about" className="container py-2 sm:py-9"> <section id="about" className="container py-2 sm:py-9">
<div className="bg-accent/50 border rounded-lg py-12"> <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"> <div className="px-6 flex flex-col-reverse md:flex-row gap-8 md:gap-12">
<Image <Image
src="/about.svg" src="/about.svg"
alt="aboutpicture" alt="aboutpicture"
width={300} width={300}
height={300} height={300}
className="w-[300px] object-contain rounded-lg flex-shrink-0" className="w-[300px] object-contain rounded-lg flex-shrink-0"
/> />
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<div className="pb-6"> <div className="pb-6">
<h2 className="text-3xl md:text-5xl font-bold"> <h2 className="text-3xl md:text-5xl font-bold">
About{" "} About{" "}
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text"> <span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
SVRJS! SVRJS!
</span> </span>
</h2> </h2>
<p className="text-lg text-muted-foreground mt-4"> <p className="text-lg text-muted-foreground mt-4">
Host a webpage, run server-side JavaScript, use mods to expand Host a webpage, run server-side JavaScript, use mods to expand
server functionality, or use it as a forward or reverse proxy. server functionality, or use it as a forward or reverse proxy.
SVRJS is a web server that runs on top of Node.JS, enabling SVRJS is a web server that runs on top of Node.JS, enabling
server-side JS on webpages. SVRJS also has an integrated log server-side JS on webpages. SVRJS also has an integrated log
viewer, log highlighter, and user management tool. viewer, log highlighter, and user management tool.
</p> </p>
</div> </div>
<Statistics /> <Statistics />
</div> </div>
</div> </div>
</div> </div>
</section> </section>
); );
}; };
export default About; export default About;

View file

@ -4,7 +4,7 @@ import {
Accordion, Accordion,
AccordionContent, AccordionContent,
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger
} from "../ui/accordion"; } from "../ui/accordion";
const Faq = () => { const Faq = () => {

View file

@ -6,102 +6,102 @@ import { FOOTERLINKS } from "@/constants";
import Logo from "./Logo"; import Logo from "./Logo";
const Footer = () => { const Footer = () => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
return ( return (
<> <>
<footer className="flex flex-col w-full transition-all bg-zinc-50 text-black dark:bg-[#0308033b] border-t dark:text-white"> <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="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 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"> <div className="flex items-center mb-6 lg:mb-0">
<Logo width={200} height={80} /> <Logo width={200} height={80} />
</div> </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 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="flex flex-col items-center md:items-start">
<div className="text-2xl font-light text-primary"> <div className="text-2xl font-light text-primary">
Quick Links Quick Links
</div> </div>
{FOOTERLINKS.otherPages.map((link) => ( {FOOTERLINKS.otherPages.map((link) => (
<span key={link.href}> <span key={link.href}>
<Link <Link
href={link.href} href={link.href}
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline" className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
> >
{link.label} {link.label}
</Link> </Link>
</span> </span>
))} ))}
</div> </div>
<div className="flex flex-col items-center md:items-start"> <div className="flex flex-col items-center md:items-start">
<h1 className="text-2xl font-light text-primary">Resources</h1> <h1 className="text-2xl font-light text-primary">Resources</h1>
{FOOTERLINKS.plans.map((link) => ( {FOOTERLINKS.plans.map((link) => (
<span key={link.href}> <span key={link.href}>
<Link <Link
href={link.href} href={link.href}
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline" className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
> >
{link.label} {link.label}
</Link> </Link>
</span> </span>
))} ))}
</div> </div>
<div className="flex flex-col items-center md:items-start"> <div className="flex flex-col items-center md:items-start">
<div className="text-2xl font-light text-primary"> <div className="text-2xl font-light text-primary">
Additional Additional
</div> </div>
{FOOTERLINKS.additional.map((link) => ( {FOOTERLINKS.additional.map((link) => (
<span key={link.href}> <span key={link.href}>
<Link <Link
href={link.href} href={link.href}
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline" className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
> >
{link.label} {link.label}
</Link> </Link>
</span> </span>
))} ))}
</div> </div>
<div className="flex flex-col items-center md:items-start"> <div className="flex flex-col items-center md:items-start">
<div className="text-2xl font-light text-primary">Social</div> <div className="text-2xl font-light text-primary">Social</div>
<p className="text-base font-light"> <p className="text-base font-light">
{FOOTERLINKS.social.supportText} {FOOTERLINKS.social.supportText}
</p> </p>
<div className="flex space-x-1 py-3"> <div className="flex space-x-1 py-3">
<Iconss /> <Iconss />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="border-t mb-6 border-gray-300 dark:border-white/30"></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"> <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"> <span className="text-sm font-light">
Copyright © 2023-{currentYear}{" "} Copyright © 2023-{currentYear}{" "}
<Link <Link
href={FOOTERLINKS.footerBottom.rightsReserved.href} href={FOOTERLINKS.footerBottom.rightsReserved.href}
className="text-primary font-semibold" className="text-primary font-semibold"
> >
{FOOTERLINKS.footerBottom.rightsReserved.label} {FOOTERLINKS.footerBottom.rightsReserved.label}
</Link> </Link>
</span> </span>
<span className="text-sm font-light"> <span className="text-sm font-light">
<Link <Link
href={FOOTERLINKS.footerBottom.termsofService.href} href={FOOTERLINKS.footerBottom.termsofService.href}
className="text-primary font-medium transition-all underline-offset-4 hover:underline" className="text-primary font-medium transition-all underline-offset-4 hover:underline"
> >
{FOOTERLINKS.footerBottom.termsofService.label}{" "} {FOOTERLINKS.footerBottom.termsofService.label}{" "}
</Link> </Link>
|{" "} |{" "}
<Link <Link
href={FOOTERLINKS.footerBottom.privacyPolicy.href} href={FOOTERLINKS.footerBottom.privacyPolicy.href}
className="text-primary font-medium transition-all underline-offset-4 hover:underline" className="text-primary font-medium transition-all underline-offset-4 hover:underline"
> >
{FOOTERLINKS.footerBottom.privacyPolicy.label} {FOOTERLINKS.footerBottom.privacyPolicy.label}
</Link> </Link>
</span> </span>
</div> </div>
</div> </div>
</footer> </footer>
</> </>
); );
}; };
export default Footer; export default Footer;

View file

@ -11,160 +11,160 @@ import { cn } from "@/lib/utils";
import Image from "next/image"; import Image from "next/image";
const happyMonkey = Happy_Monkey({ const happyMonkey = Happy_Monkey({
preload: true, preload: true,
weight: ["400"], weight: ["400"],
subsets: ["latin"], subsets: ["latin"]
}); });
const Hero = () => { const Hero = () => {
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const [command, setCommand] = useState( 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" "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">( const [selectedButton, setSelectedButton] = useState<"linux" | "docker">(
"linux" "linux"
); );
const commands = { const commands = {
linux: linux:
"curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20240509.sh > /tmp/installer.sh && sudo bash /tmp/installer.sh", "curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20240509.sh > /tmp/installer.sh && sudo bash /tmp/installer.sh",
docker: docker:
"docker pull svrjs/svrjs && docker run --name mysvrjs -d -p 80:80 --restart=always svrjs/svrjs", "docker pull svrjs/svrjs && docker run --name mysvrjs -d -p 80:80 --restart=always svrjs/svrjs"
}; };
const copyToClipboard = () => { const copyToClipboard = () => {
navigator.clipboard.writeText(command); navigator.clipboard.writeText(command);
setIsCopied(true); setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); setTimeout(() => setIsCopied(false), 2000);
}; };
const handleButtonClick = (type: "linux" | "docker") => { const handleButtonClick = (type: "linux" | "docker") => {
setCommand(commands[type]); setCommand(commands[type]);
setSelectedButton(type); setSelectedButton(type);
}; };
return ( return (
<section className="relative sm:container grid lg:grid-cols-2 place-items-center py-20 md:py-24 gap-10"> <section className="relative sm:container grid lg:grid-cols-2 place-items-center py-20 md:py-24 gap-10">
<GridPattern <GridPattern
className={cn( className={cn(
"[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]", "[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]",
"inset-x-0 inset-y-[-50%] h-[200%] opacity-30" "inset-x-0 inset-y-[-50%] h-[200%] opacity-30"
)} )}
/> />
<div className="text-center lg:text-start space-y-6"> <div className="text-center lg:text-start space-y-6">
<AnimatedGradientText className="mx-auto lg:mx-0"> <AnimatedGradientText className="mx-auto lg:mx-0">
🎉{" "} 🎉{" "}
<hr className="mx-2 h-4 w-[1px] shrink-0 bg-black dark:bg-gray-300" /> <hr className="mx-2 h-4 w-[1px] shrink-0 bg-black dark:bg-gray-300" />
<span <span
className={cn( 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` `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 Expanding server functionality
</span> </span>
</AnimatedGradientText> </AnimatedGradientText>
<main className="text-5xl md:text-6xl font-bold"> <main className="text-5xl md:text-6xl font-bold">
<h1 className="inline custom-title"> <h1 className="inline custom-title">
<span className="text-transparent bg-gradient-to-r from-green-300 to-primary bg-clip-text"> <span className="text-transparent bg-gradient-to-r from-green-300 to-primary bg-clip-text">
Simplify Simplify
</span>{" "} </span>{" "}
your server logic performance your server logic performance
</h1> </h1>
</main> </main>
<p className="text-lg text-muted-foreground md:w-10/12 mx-auto lg:mx-0"> <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 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 server-side JavaScript on webpages. SVR.JS also has an integrated log
viewer and more... viewer and more...
</p> </p>
<div className="relative mx-auto lg:mx-0 flex gap-2 flex-col-reverse lg:flex-row justify-start items-center w-fit"> <div className="relative mx-auto lg:mx-0 flex gap-2 flex-col-reverse lg:flex-row justify-start items-center w-fit">
<Button <Button
className="w-fit" className="w-fit"
onClick={copyToClipboard} onClick={copyToClipboard}
variant={!isCopied ? "secondary" : "secondary"} variant={!isCopied ? "secondary" : "secondary"}
> >
{!isCopied ? ( {!isCopied ? (
<Clipboard className="w-4 h-4 mr-2" /> <Clipboard className="w-4 h-4 mr-2" />
) : ( ) : (
<Check className="w-4 h-4 mr-2" /> <Check className="w-4 h-4 mr-2" />
)} )}
{command.slice(0, 39)}... {command.slice(0, 39)}...
</Button> </Button>
<p className="hidden lg:block">|</p> <p className="hidden lg:block">|</p>
<p className="block lg:hidden">or</p> <p className="block lg:hidden">or</p>
<Link className="w-full" href="/downloads"> <Link className="w-full" href="/downloads">
<Button className="w-full">Download</Button> <Button className="w-full">Download</Button>
</Link> </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"> <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 <Image
src="/curly-arrow.png" src="/curly-arrow.png"
width={35} width={35}
height={35} height={35}
alt="Curly arrow" alt="Curly arrow"
/> />
<span <span
className={cn( className={cn(
`mt-10 font-bold text-black -scale-x-100 text-sm ${happyMonkey.className}` `mt-10 font-bold text-black -scale-x-100 text-sm ${happyMonkey.className}`
)} )}
> >
Try Now! Try Now!
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center lg:justify-start justify-center gap-3 w-full"> <div className="flex items-center lg:justify-start justify-center gap-3 w-full">
<Button <Button
className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${ className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${
selectedButton === "linux" selectedButton === "linux"
? "bg-accent" ? "bg-accent"
: "bg-primary-foreground/20" : "bg-primary-foreground/20"
}`} }`}
variant={"ghost"} variant={"ghost"}
onClick={() => handleButtonClick("linux")} onClick={() => handleButtonClick("linux")}
> >
<span className="sr-only">Linux</span> <span className="sr-only">Linux</span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={200} width={200}
height={200} height={200}
viewBox="0 0 448 512" viewBox="0 0 448 512"
fill="none" fill="none"
> >
<path <path
fill="currentColor" 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" 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> </svg>
</Button> </Button>
<Button <Button
className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${ className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${
selectedButton === "docker" selectedButton === "docker"
? "bg-accent" ? "bg-accent"
: "bg-primary-foreground/20" : "bg-primary-foreground/20"
}`} }`}
variant={"ghost"} variant={"ghost"}
onClick={() => handleButtonClick("docker")} onClick={() => handleButtonClick("docker")}
> >
<span className="sr-only">Docker</span> <span className="sr-only">Docker</span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={200} width={200}
height={200} height={200}
viewBox="0 0 50 50" viewBox="0 0 50 50"
fill="none" fill="none"
> >
<path <path
fill="currentColor" 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" 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> </svg>
</Button> </Button>
</div> </div>
</div> </div>
<div className="z-10"> <div className="z-10">
<HeroCards /> <HeroCards />
</div> </div>
<div className="shadow"></div> <div className="shadow"></div>
</section> </section>
); );
}; };
export default Hero; export default Hero;

View file

@ -5,12 +5,12 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle
} from "@/components/ui/card"; } from "@/components/ui/card";
import Image from "next/image"; import Image from "next/image";
import { Happy_Monkey } from "next/font/google"; import { Happy_Monkey } from "next/font/google";
@ -19,170 +19,170 @@ import Iconss from "../ui/icons";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
const happyMonkey = Happy_Monkey({ const happyMonkey = Happy_Monkey({
preload: true, preload: true,
weight: ["400"], weight: ["400"],
subsets: ["latin"], subsets: ["latin"]
}); });
const HeroCards = () => { const HeroCards = () => {
const router = useRouter(); const router = useRouter();
const cards = { const cards = {
aboutCard: { aboutCard: {
description: description:
"Discover more about our services and offerings. We aim to provide the best experience for our users.", "Discover more about our services and offerings. We aim to provide the best experience for our users.",
socialLinks: { socialLinks: {
x: "https://x.com/SVR_JS", x: "https://x.com/SVR_JS",
Mastodon: "https://mastodon.social/@svrjs", Mastodon: "https://mastodon.social/@svrjs",
Bluesky: "https://bsky.app/profile/svrjs.org", Bluesky: "https://bsky.app/profile/svrjs.org",
Odysee: "https://odysee.com/@SVRJS", Odysee: "https://odysee.com/@SVRJS"
}, }
}, },
pricingCard: { pricingCard: {
planName: "Pro Plan", planName: "Pro Plan",
badgeTitle: "Popular", badgeTitle: "Popular",
pricePerMonth: "$0", pricePerMonth: "$0",
description: description:
"Get the best features and priority support with our Pro Plan.", "Get the best features and priority support with our Pro Plan.",
primaryButtonText: "Download SVR Now", primaryButtonText: "Download SVR Now",
onPrimaryButtonClick: () => router.push("/downloads"), onPrimaryButtonClick: () => router.push("/downloads"),
features: [ features: [
{ {
title: "Unlimited Projects", title: "Unlimited Projects",
icons: <Infinity className="rounded-full" width={25} height={25} />, icons: <Infinity className="rounded-full" width={25} height={25} />
}, },
{ {
title: "Priority Support", title: "Priority Support",
icons: ( icons: (
<ArchiveRestore className="rounded-full" width={25} height={25} /> <ArchiveRestore className="rounded-full" width={25} height={25} />
), )
}, },
{ {
title: "Free Updates", title: "Free Updates",
icons: <Headset className="rounded-full" width={25} height={25} />, icons: <Headset className="rounded-full" width={25} height={25} />
}, }
], ],
curlyText: "Best Value!", curlyText: "Best Value!"
}, },
serviceCard: { serviceCard: {
title: "Our Services", title: "Our Services",
description: description:
"We offer a variety of services to cater to your needs, including web development, SEO, and more.", "We offer a variety of services to cater to your needs, including web development, SEO, and more."
}, }
}; };
return ( return (
<div className="hidden lg:flex flex-row flex-wrap gap-8 relative w-[700px] h-[500px]"> <div className="hidden lg:flex flex-row flex-wrap gap-8 relative w-[700px] h-[500px]">
{/* Twitter First Top left */} {/* Twitter First Top left */}
<Card className="absolute w-[340px] -top-[15px] drop-shadow-xl shadow-black/10 dark:shadow-white/10"> <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"> <CardHeader className="flex flex-row items-center gap-4 pb-2">
<Avatar> <Avatar>
<AvatarImage <AvatarImage
alt={"twitteravatar"} alt={"twitteravatar"}
src={"https://github.com/shadcn.png"} src={"https://github.com/shadcn.png"}
/> />
<AvatarFallback>Proxy</AvatarFallback> <AvatarFallback>Proxy</AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-col"> <div className="flex flex-col">
<CardTitle className="text-lg">Proxy</CardTitle> <CardTitle className="text-lg">Proxy</CardTitle>
<CardDescription>@proxyxd_s</CardDescription> <CardDescription>@proxyxd_s</CardDescription>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
Svrjs has the best server side rendering{" "} Svrjs has the best server side rendering{" "}
<span className="text-sky-400">#SVRJSONTOP</span> <span className="text-sky-400">#SVRJSONTOP</span>
</CardContent> </CardContent>
<BorderBeam className="-z-10" /> <BorderBeam className="-z-10" />
</Card> </Card>
{/* Socials Second top right */} {/* 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"> <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"> <CardHeader className="flex justify-center items-center pb-2">
<CardTitle className="text-center">Socials</CardTitle> <CardTitle className="text-center">Socials</CardTitle>
<CardDescription className="font-medium text-primary"></CardDescription> <CardDescription className="font-medium text-primary"></CardDescription>
</CardHeader> </CardHeader>
<CardContent className="text-center text-muted-foreground pb-2"> <CardContent className="text-center text-muted-foreground pb-2">
<p>{cards.aboutCard.description}</p> <p>{cards.aboutCard.description}</p>
</CardContent> </CardContent>
<CardFooter> <CardFooter>
<Iconss /> <Iconss />
</CardFooter> </CardFooter>
<BorderBeam className="-z-10" /> <BorderBeam className="-z-10" />
</Card> </Card>
{/* Pricings Bottom left */} {/* Pricings Bottom left */}
<Card className="absolute top-[170px] left-[50px] w-72 drop-shadow-xl shadow-black/10 dark:shadow-white/10"> <Card className="absolute top-[170px] left-[50px] w-72 drop-shadow-xl shadow-black/10 dark:shadow-white/10">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center justify-between"> <CardTitle className="flex items-center justify-between">
{cards.pricingCard.planName} {cards.pricingCard.planName}
<Badge variant="secondary" className="text-sm text-primary"> <Badge variant="secondary" className="text-sm text-primary">
{cards.pricingCard.badgeTitle} {cards.pricingCard.badgeTitle}
</Badge> </Badge>
</CardTitle> </CardTitle>
<div> <div>
<span className="text-3xl font-bold"> <span className="text-3xl font-bold">
{cards.pricingCard.pricePerMonth} {cards.pricingCard.pricePerMonth}
</span> </span>
<span className="text-muted-foreground"> /month</span> <span className="text-muted-foreground"> /month</span>
</div> </div>
<CardDescription>{cards.pricingCard.description}</CardDescription> <CardDescription>{cards.pricingCard.description}</CardDescription>
<Button <Button
size="sm" size="sm"
className="w-full" className="w-full"
onClick={cards.pricingCard.onPrimaryButtonClick} onClick={cards.pricingCard.onPrimaryButtonClick}
> >
{cards.pricingCard.primaryButtonText} {cards.pricingCard.primaryButtonText}
</Button> </Button>
</CardHeader> </CardHeader>
<hr className="w-4/5 m-auto -mt-2 mb-4" /> <hr className="w-4/5 m-auto -mt-2 mb-4" />
<CardFooter className="flex"> <CardFooter className="flex">
<div className="space-y-3"> <div className="space-y-3">
{cards.pricingCard.features.map((benefit) => ( {cards.pricingCard.features.map((benefit) => (
<span <span
key={benefit.title} key={benefit.title}
className="inline-flex justify-center items-center gap-x-3" className="inline-flex justify-center items-center gap-x-3"
> >
{benefit.icons} {benefit.icons}
<h3>{benefit.title}</h3> <h3>{benefit.title}</h3>
</span> </span>
))} ))}
</div> </div>
</CardFooter> </CardFooter>
<BorderBeam className="-z-10" /> <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"> <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 <Image
src="/curly-arrow.png" src="/curly-arrow.png"
width={35} width={35}
height={35} height={35}
alt="Curly arrow" alt="Curly arrow"
/> />
<span <span
style={happyMonkey.style} style={happyMonkey.style}
className="mt-10 font-bold text-black -scale-x-100 text-sm" className="mt-10 font-bold text-black -scale-x-100 text-sm"
> >
{cards.pricingCard.curlyText} {cards.pricingCard.curlyText}
</span> </span>
</div> </div>
</Card> </Card>
{/* Service */} {/* Service */}
<Card className="absolute w-[350px] -right-[10px] bottom-[75px] drop-shadow-xl shadow-black/10 dark:shadow-white/10"> <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"> <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"> <div className="mt-1 bg-primary/20 p-1 rounded-2xl">
<LightbulbIcon className="fill-black dark:fill-[#F596D3]" /> <LightbulbIcon className="fill-black dark:fill-[#F596D3]" />
</div> </div>
<div> <div>
<CardTitle>{cards.serviceCard.title}</CardTitle> <CardTitle>{cards.serviceCard.title}</CardTitle>
<CardDescription className="text-md mt-2"> <CardDescription className="text-md mt-2">
{cards.serviceCard.description} {cards.serviceCard.description}
</CardDescription> </CardDescription>
</div> </div>
</CardHeader> </CardHeader>
<BorderBeam className="-z-10" /> <BorderBeam className="-z-10" />
</Card> </Card>
</div> </div>
); );
}; };
export default HeroCards; export default HeroCards;

View file

@ -3,33 +3,33 @@ import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
const HowItWorks = () => { const HowItWorks = () => {
return ( return (
<section className="container text-center py-12 sm:py-24"> <section className="container text-center py-12 sm:py-24">
<h2 className="text-3xl md:text-5xl font-bold"> <h2 className="text-3xl md:text-5xl font-bold">
Accelerate your{" "} Accelerate your{" "}
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text"> <span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
development development
</span> </span>
</h2> </h2>
<p className="md:w-3/4 mx-auto mt-4 mb-8 text-lg md:text-xl text-muted-foreground"> <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 Beautifully designed components that you can copy and paste into your
apps. Accessible. Customizable. Open Source. apps. Accessible. Customizable. Open Source.
</p> </p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{Features.map(({ icon, title, description }) => ( {Features.map(({ icon, title, description }) => (
<Card key={title} className="bg-muted/50"> <Card key={title} className="bg-muted/50">
<CardHeader> <CardHeader>
<CardTitle className="grid gap-4 place-items-center"> <CardTitle className="grid gap-4 place-items-center">
{icon} {icon}
{title} {title}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent>{description}</CardContent> <CardContent>{description}</CardContent>
</Card> </Card>
))} ))}
</div> </div>
</section> </section>
); );
}; };
export default HowItWorks; export default HowItWorks;

View file

@ -1,9 +1,9 @@
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger
} from "../ui/sheet"; } from "../ui/sheet";
import { Menu } from "lucide-react"; import { Menu } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
@ -14,63 +14,63 @@ import { buttonVariants } from "../ui/button";
import Logo from "./Logo"; import Logo from "./Logo";
const MobileNav = () => { const MobileNav = () => {
return ( return (
<div className="flex md:hidden"> <div className="flex md:hidden">
<ThemeToggle /> <ThemeToggle />
<Sheet> <Sheet>
<SheetTrigger> <SheetTrigger>
<span className="sr-only">Menu</span> <span className="sr-only">Menu</span>
<Menu className="w-5 h-5" /> <Menu className="w-5 h-5" />
</SheetTrigger> </SheetTrigger>
<SheetContent> <SheetContent>
<SheetHeader> <SheetHeader>
<SheetTitle> <SheetTitle>
<Logo width={120} height={40} /> <Logo width={120} height={40} />
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
<nav <nav
id="mobile-menu" id="mobile-menu"
className="flex flex-col justify-center items-center gap-2 mt-4" className="flex flex-col justify-center items-center gap-2 mt-4"
> >
{NAVBAR.centerLinks?.map(({ href = "", label, target }) => ( {NAVBAR.centerLinks?.map(({ href = "", label, target }) => (
<Link <Link
key={label} key={label}
href={href} href={href}
target={target} target={target}
className={buttonVariants({ variant: "ghost" })} className={buttonVariants({ variant: "ghost" })}
> >
{label} {label}
</Link> </Link>
))} ))}
{NAVBAR.rightLinks?.map(({ href, label, target }) => ( {NAVBAR.rightLinks?.map(({ href, label, target }) => (
<Link <Link
key={label} key={label}
href={href} href={href}
target={target} target={target}
className={`w-[110px] gap-2 ${buttonVariants({ className={`w-[110px] gap-2 ${buttonVariants({
variant: "secondary", variant: "secondary"
})}`} })}`}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={25} width={25}
height={25} height={25}
viewBox="0 0 123 123" viewBox="0 0 123 123"
fill="none" fill="none"
> >
<path <path
fill="currentColor" 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" 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> </svg>
{label} {label}
</Link> </Link>
))} ))}
</nav> </nav>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
</div> </div>
); );
}; };
export default MobileNav; export default MobileNav;

View file

@ -1,9 +1,9 @@
"use client"; "use client";
import { import {
NavigationMenu, NavigationMenu,
NavigationMenuItem, NavigationMenuItem,
NavigationMenuList, NavigationMenuList
} from "@radix-ui/react-navigation-menu"; } from "@radix-ui/react-navigation-menu";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@ -15,76 +15,76 @@ import { usePathname } from "next/navigation";
import Logo from "./Logo"; import Logo from "./Logo";
const Navbar = () => { const Navbar = () => {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<header className="sticky border-b top-0 z-40 w-full shadow-md bg-white dark:border-b-slate-800 dark:bg-background"> <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 */} {/* LOGO LEFT NAVBAR */}
<NavigationMenu className="mx-auto"> <NavigationMenu className="mx-auto">
<NavigationMenuList className="container h-16 px-4 w-full flex justify-between items-center"> <NavigationMenuList className="container h-16 px-4 w-full flex justify-between items-center">
<NavigationMenuItem className="font-bold flex items-center"> <NavigationMenuItem className="font-bold flex items-center">
<Link href="/#" className="inline-flex items-center gap-2"> <Link href="/#" className="inline-flex items-center gap-2">
<span className="sr-only">SVR.JS</span> <span className="sr-only">SVR.JS</span>
<Logo width={120} height={40} /> <Logo width={120} height={40} />
</Link> </Link>
</NavigationMenuItem> </NavigationMenuItem>
{/* Mobile view */} {/* Mobile view */}
<NavigationMenuItem className="flex md:hidden"> <NavigationMenuItem className="flex md:hidden">
<MobileNav /> <MobileNav />
</NavigationMenuItem> </NavigationMenuItem>
{/* Desktop Menu */} {/* Desktop Menu */}
<NavigationMenuItem className="hidden md:flex"> <NavigationMenuItem className="hidden md:flex">
<nav className="hidden md:flex gap-4"> <nav className="hidden md:flex gap-4">
{NAVBAR.centerLinks?.map(({ href, label, target }) => ( {NAVBAR.centerLinks?.map(({ href, label, target }) => (
<Link <Link
key={label} key={label}
href={href} href={href}
target={target} target={target}
className={`text-[17px] tracking-tight ${ className={`text-[17px] tracking-tight ${
pathname == href ? "bg-muted-foreground/20" : "" pathname == href ? "bg-muted-foreground/20" : ""
} ${buttonVariants({ } ${buttonVariants({
variant: "ghost", variant: "ghost"
})}`} })}`}
> >
{label} {label}
</Link> </Link>
))} ))}
</nav> </nav>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem className="hidden md:flex gap-2 items-center"> <NavigationMenuItem className="hidden md:flex gap-2 items-center">
{NAVBAR.rightLinks?.map(({ href = "", label, target }) => ( {NAVBAR.rightLinks?.map(({ href = "", label, target }) => (
<Link <Link
key={label} key={label}
href={href} href={href}
target={target} target={target}
className={`border ${buttonVariants({ className={`border ${buttonVariants({
variant: "ghost", variant: "ghost",
size: "icon", size: "icon"
})}`} })}`}
> >
<span className="sr-only">Git</span> <span className="sr-only">Git</span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={25} width={25}
height={25} height={25}
viewBox="0 0 123 123" viewBox="0 0 123 123"
fill="none" fill="none"
> >
<path <path
fill="currentColor" 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" 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> </svg>
</Link> </Link>
))} ))}
<ThemeToggle /> <ThemeToggle />
</NavigationMenuItem> </NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
</header> </header>
); );
}; };
export default Navbar; export default Navbar;

View file

@ -9,138 +9,138 @@ import { Mail } from "lucide-react";
import HCaptcha from "@hcaptcha/react-hcaptcha"; import HCaptcha from "@hcaptcha/react-hcaptcha";
const happyMonkey = Happy_Monkey({ const happyMonkey = Happy_Monkey({
preload: true, preload: true,
weight: "400", weight: "400",
subsets: ["latin"], subsets: ["latin"]
}); });
const Newsletter = () => { const Newsletter = () => {
const [submission, setSubmission] = useState< const [submission, setSubmission] = useState<
"idle" | "loading" | "success" | "error" "idle" | "loading" | "success" | "error"
>("idle"); >("idle");
const [input, setInput] = useState<string>(""); const [input, setInput] = useState<string>("");
const [captchaToken, setCaptchaToken] = useState<string | null>(null); const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [showCaptcha, setShowCaptcha] = useState<boolean>(false); const [showCaptcha, setShowCaptcha] = useState<boolean>(false);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Added this line const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Added this line
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const hcaptchaRef = useRef<HCaptcha>(null); const hcaptchaRef = useRef<HCaptcha>(null);
const handleCaptcha = async (token: string) => { const handleCaptcha = async (token: string) => {
setCaptchaToken(token); setCaptchaToken(token);
setShowCaptcha(false); setShowCaptcha(false);
await handleSubmit(token); await handleSubmit(token);
}; };
const handleSubmit = async (token: string | null) => { const handleSubmit = async (token: string | null) => {
if (!input || !token || isSubmitting) return; if (!input || !token || isSubmitting) return;
setIsSubmitting(true); setIsSubmitting(true);
setSubmission("loading"); setSubmission("loading");
try { try {
const response = await fetch("/api/subscribe", { const response = await fetch("/api/subscribe", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, },
body: JSON.stringify({ email: input, captchaToken: token }), body: JSON.stringify({ email: input, captchaToken: token })
}); });
if (response.ok) { if (response.ok) {
setSubmission("success"); setSubmission("success");
setInput(""); setInput("");
} else { } else {
setSubmission("error"); setSubmission("error");
} }
} catch (error) { } catch (error) {
console.error("Error subscribing:", error); console.error("Error subscribing:", error);
setSubmission("error"); setSubmission("error");
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
const handleSubscribeClick = () => { const handleSubscribeClick = () => {
if (!input) return; if (!input) return;
setShowCaptcha(true); setShowCaptcha(true);
}; };
return ( return (
<section id="newsletter"> <section id="newsletter">
<hr className="w-11/12 mx-auto" /> <hr className="w-11/12 mx-auto" />
<div className="container py-24 md:py-32"> <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"> <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! Join The Newsletter!
</h3> </h3>
<p className="text-lg text-muted-foreground text-center mt-4 md:mt-2 mb-8"> <p className="text-lg text-muted-foreground text-center mt-4 md:mt-2 mb-8">
Subscribe to our newsletter for updates. We promise no spam emails Subscribe to our newsletter for updates. We promise no spam emails
will be sent will be sent
</p> </p>
<form <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" 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" aria-label="Email Information"
onSubmit={(e) => e.preventDefault()} onSubmit={(e) => e.preventDefault()}
> >
<div className="group flex items-center gap-x-4 pl-4 pr-1 rounded-[9px] bg-accent/80 hover:bg-accent shadow-outline-gray hover:shadow-transparent focus-within:bg-accent focus-within:!shadow-outline-gray-focus transition-all duration-300"> <div className="group flex items-center gap-x-4 pl-4 pr-1 rounded-[9px] bg-accent/80 hover:bg-accent shadow-outline-gray hover:shadow-transparent focus-within:bg-accent focus-within:!shadow-outline-gray-focus transition-all duration-300">
<Mail className="hidden sm:inline w-6 h-6 text-[#4B4C52] group-focus-within:text-secondary-foreground group-hover:text-secondary-foreground transition-colors duration-300" /> <Mail className="hidden sm:inline w-6 h-6 text-[#4B4C52] group-focus-within:text-secondary-foreground group-hover:text-secondary-foreground transition-colors duration-300" />
<Input <Input
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
placeholder="Email address" placeholder="Email address"
required required
type="email" type="email"
className="flex-1 text-secondary-foreground text-sm sm:text-base outline-none placeholder-[#4B4C52] group-focus-within:placeholder-muted bg-transparent placeholder:transition-colors placeholder:duration-300 border-none" className="flex-1 text-secondary-foreground text-sm sm:text-base outline-none placeholder-[#4B4C52] group-focus-within:placeholder-muted bg-transparent placeholder:transition-colors placeholder:duration-300 border-none"
/> />
</div> </div>
<Button <Button
ref={buttonRef} ref={buttonRef}
onClick={handleSubscribeClick} onClick={handleSubscribeClick}
disabled={submission === "loading" || !input || isSubmitting} disabled={submission === "loading" || !input || isSubmitting}
> >
Subscribe Subscribe
</Button> </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"> <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 <Image
src="/curly-arrow.png" src="/curly-arrow.png"
alt="see here" alt="see here"
width={35} width={35}
height={35} height={35}
/> />
<span <span
className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`} className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`}
> >
{submission === "idle" && "Subscribe Now"} {submission === "idle" && "Subscribe Now"}
{submission === "loading" && ( {submission === "loading" && (
<p className="text-sm text-center">Subscribing...</p> <p className="text-sm text-center">Subscribing...</p>
)} )}
{submission === "success" && ( {submission === "success" && (
<p className="dark:invert text-sm text-center text-green-500"> <p className="dark:invert text-sm text-center text-green-500">
🎉 Subscribed successfully... 🎉 Subscribed successfully...
</p> </p>
)} )}
{submission === "error" && ( {submission === "error" && (
<p className="dark:invert text-sm text-center text-red-500"> <p className="dark:invert text-sm text-center text-red-500">
😥 Something went wrong... 😥 Something went wrong...
</p> </p>
)} )}
</span> </span>
</div> </div>
</form> </form>
{showCaptcha && ( {showCaptcha && (
<div className="flex-center relative"> <div className="flex-center relative">
<HCaptcha <HCaptcha
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!} sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
onVerify={handleCaptcha} onVerify={handleCaptcha}
ref={hcaptchaRef} ref={hcaptchaRef}
/> />
</div> </div>
)} )}
</div> </div>
<hr className="w-11/12 mx-auto" /> <hr className="w-11/12 mx-auto" />
</section> </section>
); );
}; };
export default Newsletter; export default Newsletter;

View file

@ -7,50 +7,50 @@ import { useRouter } from "next/navigation";
import HeroVideoDialog from "../ui/heroVideoAction"; import HeroVideoDialog from "../ui/heroVideoAction";
const Partners = () => { const Partners = () => {
const router = useRouter(); const router = useRouter();
const handleClick = () => { const handleClick = () => {
router.push("/docs"); router.push("/docs");
}; };
return ( return (
<section <section
id="partners" id="partners"
className="wrapper container py-24 md:py-28 gap-4 flex flex-col" className="wrapper container py-24 md:py-28 gap-4 flex flex-col"
> >
<h2 className="text-3xl md:text-5xl font-bold text-start"> <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"> <span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
SVRJS SVRJS
</span>{" "} </span>{" "}
in action in action
</h2> </h2>
<div className="w-full flex-start flex-row"> <div className="w-full flex-start flex-row">
<div className="flex max-md:flex-col items-center justify-start gap-4"> <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"> <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. Process of setting up a WordPress website running on SVR.JS.
</h2> </h2>
<Button <Button
onClick={handleClick} onClick={handleClick}
className="flex-center font-bold max-md:w-full max-w-xl" className="flex-center font-bold max-md:w-full max-w-xl"
> >
Docs <ArrowUpRight /> Docs <ArrowUpRight />
</Button> </Button>
</div> </div>
</div> </div>
<HeroVideoDialog <HeroVideoDialog
animationStyle="top-in-bottom-out" animationStyle="top-in-bottom-out"
videoSrc="https://odysee.com/$/embed/@SVRJS:5/svrjs-in-action:e?r=7t9EG6VDTNZDSze8ysoChqocLNhAMZEe" videoSrc="https://odysee.com/$/embed/@SVRJS:5/svrjs-in-action:e?r=7t9EG6VDTNZDSze8ysoChqocLNhAMZEe"
thumbnailSrc="/poster.svg" thumbnailSrc="/poster.svg"
thumbnailAlt="Poster.svg" thumbnailAlt="Poster.svg"
/> />
{/* <video {/* <video
src="/svgaction.mp4" src="/svgaction.mp4"
className="rounded-xl aspect-video bg-[#09090b]" className="rounded-xl aspect-video bg-[#09090b]"
controls controls
poster="/poster.svg" poster="/poster.svg"
></video> */} ></video> */}
<hr className="w-full h-1" /> <hr className="w-full h-1" />
</section> </section>
); );
}; };
export default Partners; export default Partners;

View file

@ -2,20 +2,20 @@ import { stats } from "@/constants";
import NumberTicker from "../widgets/num-tick"; import NumberTicker from "../widgets/num-tick";
const Statistics = () => { const Statistics = () => {
return ( return (
<section> <section>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{stats.map(({ title, count }) => ( {stats.map(({ title, count }) => (
<div key={title} className="space-y-2 text-center"> <div key={title} className="space-y-2 text-center">
<h2 className="text-3xl sm:text-4xl font-bold"> <h2 className="text-3xl sm:text-4xl font-bold">
<NumberTicker value={count} /> <NumberTicker value={count} />
</h2> </h2>
<p className="text-xl text-muted-foreground">{title}</p> <p className="text-xl text-muted-foreground">{title}</p>
</div> </div>
))} ))}
</div> </div>
</section> </section>
); );
}; };
export default Statistics; export default Statistics;

View file

@ -2,104 +2,104 @@ import React from "react";
import TestimonialCard from "../cards/testimonialCard"; import TestimonialCard from "../cards/testimonialCard";
const testimonials = [ const testimonials = [
{ {
name: "John Doe", name: "John Doe",
role: "CEO, Example Corp.", role: "CEO, Example Corp.",
avatar: "avatar1", avatar: "avatar1",
testimonial: testimonial:
"Working with this team was a fantastic experience. They developed our website exactly to our specifications, and everything was seamless and well-integrated.", "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, rating: 5
}, },
{ {
name: "Jane Smith", name: "Jane Smith",
role: "CEO, CleanCo", role: "CEO, CleanCo",
avatar: "avatar2", avatar: "avatar2",
testimonial: testimonial:
"We're thrilled with the website. It's simple, clean, and has significantly boosted our sales. The developers did an excellent job.", "We're thrilled with the website. It's simple, clean, and has significantly boosted our sales. The developers did an excellent job.",
rating: 4, rating: 4
}, },
{ {
name: "Sam Green", name: "Sam Green",
role: "Web Developer", role: "Web Developer",
avatar: "avatar3", avatar: "avatar3",
testimonial: testimonial:
"Collaborating with this team to build a SaaS-integrated website was a perfect experience. I look forward to working with them again.", "Collaborating with this team to build a SaaS-integrated website was a perfect experience. I look forward to working with them again.",
rating: 5, rating: 5
}, },
{ {
name: "Chris Brown", name: "Chris Brown",
role: "Web Coder", role: "Web Coder",
avatar: "avatar4", avatar: "avatar4",
testimonial: testimonial:
"The team's understanding of our needs and their ability to provide fitting solutions was impressive. Their support and guidance were invaluable.", "The team's understanding of our needs and their ability to provide fitting solutions was impressive. Their support and guidance were invaluable.",
rating: 4, rating: 4
}, },
{ {
name: "Alex Johnson", name: "Alex Johnson",
avatar: "avatar5", avatar: "avatar5",
testimonial: testimonial:
"Exceptional service and outstanding results. They consistently deliver on time and within budget, making them our go-to partner for all our projects.", "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, rating: 5
}, },
{ {
name: "Patricia Taylor", name: "Patricia Taylor",
role: "Web Developer", role: "Web Developer",
avatar: "avatar6", avatar: "avatar6",
testimonial: testimonial:
"It was great to work with them. I needed a design for a SaaS project, and it was delivered within 2 days.", "It was great to work with them. I needed a design for a SaaS project, and it was delivered within 2 days.",
rating: 4, rating: 4
}, },
{ {
name: "Emily Davis", name: "Emily Davis",
role: "UX Designer, Creative Agency", role: "UX Designer, Creative Agency",
avatar: "avatar7", avatar: "avatar7",
testimonial: testimonial:
"Collaborating with them has been a pleasure. Their creativity and user-centric approach have significantly enhanced our product's usability.", "Collaborating with them has been a pleasure. Their creativity and user-centric approach have significantly enhanced our product's usability.",
rating: 5, rating: 5
}, },
{ {
name: "Michael Lee", name: "Michael Lee",
avatar: "avatar8", avatar: "avatar8",
testimonial: testimonial:
"They have a keen understanding of our business needs and consistently deliver top-notch solutions. Their reliability and efficiency are commendable.", "They have a keen understanding of our business needs and consistently deliver top-notch solutions. Their reliability and efficiency are commendable.",
rating: 5, rating: 5
}, },
{ {
name: "Sarah Wilson", name: "Sarah Wilson",
avatar: "avatar9", avatar: "avatar9",
testimonial: testimonial:
"Their dedication to client satisfaction is evident in everything they do. We've seen remarkable improvements in our processes thanks to their expertise.", "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, rating: 4
}, }
]; ];
const Testimonials = () => { const Testimonials = () => {
return ( return (
<section className="mx-auto flex w-full max-w-7xl flex-col pt-12 md:pt-24"> <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"> <div className="flex flex-row items-center justify-center space-x-1">
<span className="text-white/50 text-xs lg:text-base">Testimonials</span> <span className="text-white/50 text-xs lg:text-base">Testimonials</span>
</div> </div>
<h1 className="text-3xl md:text-5xl font-bold text-center"> <h1 className="text-3xl md:text-5xl font-bold text-center">
Hear it from{" "} Hear it from{" "}
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text"> <span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
our users our users
</span> </span>
</h1> </h1>
<ul className="wrapper columns-1 gap-5 md:columns-2 lg:columns-3 py-6 mt-6"> <ul className="wrapper columns-1 gap-5 md:columns-2 lg:columns-3 py-6 mt-6">
{testimonials.map((testimonial, idx) => ( {testimonials.map((testimonial, idx) => (
<TestimonialCard <TestimonialCard
avatar={testimonial.avatar} avatar={testimonial.avatar}
name={testimonial.name} name={testimonial.name}
role={testimonial.role} role={testimonial.role}
testimonial={testimonial.testimonial} testimonial={testimonial.testimonial}
rating={testimonial.rating} rating={testimonial.rating}
key={idx} key={idx}
/> />
))} ))}
</ul> </ul>
</section> </section>
); );
}; };
export default Testimonials; export default Testimonials;

View file

@ -5,25 +5,25 @@ import { Button } from "../ui/button";
import { Check, Copy } from "lucide-react"; import { Check, Copy } from "lucide-react";
export default function CopyButton({ code }: { code: string }) { export default function CopyButton({ code }: { code: string }) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const copyCode = async () => { const copyCode = async () => {
try { try {
await navigator.clipboard.writeText(code); await navigator.clipboard.writeText(code);
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
} catch (error) { } catch (error) {
console.error("Failed to copy!", error); console.error("Failed to copy!", error);
} }
}; };
return ( return (
<Button <Button
onClick={copyCode} onClick={copyCode}
className="absolute top-2 right-2 bg-accent hover:bg-muted text-white p-2 rounded" className="absolute top-2 right-2 bg-accent hover:bg-muted text-white p-2 rounded"
size={"icon"} size={"icon"}
> >
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />} {copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
</Button> </Button>
); );
} }

View file

@ -2,7 +2,7 @@
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
export default function AuthProvider({ export default function AuthProvider({
children, children
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {

View file

@ -1,17 +1,17 @@
import React from "react"; import React from "react";
interface ChangelogLayoutProps { interface ChangelogLayoutProps {
children: React.ReactNode; children: React.ReactNode;
} }
export const ChangelogLayout: React.FC<ChangelogLayoutProps> = ({ export const ChangelogLayout: React.FC<ChangelogLayoutProps> = ({
children, children
}) => { }) => {
return ( return (
<div className="wrapper container py-24 md:py-28 gap-4 flex flex-col"> <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"> <div className="prose max-w-full prose-lg dark:prose-invert">
{children} {children}
</div> </div>
</div> </div>
); );
}; };

View file

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes" import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types" import { type ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider> return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
} }

View file

@ -9,48 +9,48 @@ import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root; const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef< const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>, React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AccordionPrimitive.Item <AccordionPrimitive.Item
ref={ref} ref={ref}
className={cn("border-b", className)} className={cn("border-b", className)}
{...props} {...props}
/> />
)); ));
AccordionItem.displayName = "AccordionItem"; AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef< const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>, React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-45", "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-45",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<PlusIcon className="h-5 w-5 shrink-0 transition-transform duration-200" /> <PlusIcon className="h-5 w-5 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
)); ));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef< const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>, React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content <AccordionPrimitive.Content
ref={ref} ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props} {...props}
> >
<div className={cn("pb-4 pt-0", className)}>{children}</div> <div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
)); ));
AccordionContent.displayName = AccordionPrimitive.Content.displayName; AccordionContent.displayName = AccordionPrimitive.Content.displayName;

View file

@ -3,7 +3,7 @@ import { ReactNode } from "react";
export default function AnimatedGradientText({ export default function AnimatedGradientText({
children, children,
className, className
}: { }: {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
@ -12,7 +12,7 @@ export default function AnimatedGradientText({
<div <div
className={cn( 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", "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, className
)} )}
> >
<div <div

View file

@ -10,13 +10,13 @@ interface AnimatedShinyTextProps {
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
children, children,
className, className,
shimmerWidth = 100, shimmerWidth = 100
}) => { }) => {
return ( return (
<p <p
style={ style={
{ {
"--shimmer-width": `${shimmerWidth}px`, "--shimmer-width": `${shimmerWidth}px`
} as CSSProperties } as CSSProperties
} }
className={cn( className={cn(
@ -28,7 +28,7 @@ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
// Shimmer gradient // Shimmer gradient
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80", "bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
className, className
)} )}
> >
{children} {children}

View file

@ -13,7 +13,7 @@ const Avatar = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className, className
)} )}
{...props} {...props}
/> />
@ -40,7 +40,7 @@ const AvatarFallback = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted", "flex h-full w-full items-center justify-center rounded-full bg-muted",
className, className
)} )}
{...props} {...props}
/> />

View file

@ -14,13 +14,13 @@ const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground", outline: "text-foreground"
}, }
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default"
}, }
}, }
); );
export interface BadgeProps export interface BadgeProps

View file

@ -19,7 +19,7 @@ export const BorderBeam = ({
borderWidth = 1.5, borderWidth = 1.5,
colorFrom = "#8803AF", colorFrom = "#8803AF",
colorTo = "#61DAFB", colorTo = "#61DAFB",
delay = 0, delay = 0
}: BorderBeamProps) => { }: BorderBeamProps) => {
return ( return (
<div <div
@ -31,7 +31,7 @@ export const BorderBeam = ({
"--border-width": borderWidth, "--border-width": borderWidth,
"--color-from": colorFrom, "--color-from": colorFrom,
"--color-to": colorTo, "--color-to": colorTo,
"--delay": `-${delay}s`, "--delay": `-${delay}s`
} as React.CSSProperties } as React.CSSProperties
} }
className={cn( className={cn(

View file

@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( 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", "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",
@ -17,40 +17,40 @@ const buttonVariants = cva(
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline"
}, },
size: { size: {
default: "h-10 px-4 py-2", default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3", sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8", lg: "h-11 rounded-md px-8",
icon: "h-10 w-10", icon: "h-10 w-10"
}, }
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default"
}, }
} }
) );
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} }
) );
Button.displayName = "Button" Button.displayName = "Button";
export { Button, buttonVariants } export { Button, buttonVariants };

View file

@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Card = React.forwardRef< const Card = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@ -14,8 +14,8 @@ const Card = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
Card.displayName = "Card" Card.displayName = "Card";
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)} className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} {...props}
/> />
)) ));
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
@ -41,8 +41,8 @@ const CardTitle = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef< const CardContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)) ));
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)} className={cn("flex items-center p-6 pt-0", className)}
{...props} {...props}
/> />
)) ));
CardFooter.displayName = "CardFooter" CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent
};

View file

@ -1,18 +1,18 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react" import { X } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ const DialogHeader = ({
className, className,
@ -64,8 +64,8 @@ const DialogHeader = ({
)} )}
{...props} {...props}
/> />
) );
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ const DialogFooter = ({
className, className,
@ -78,8 +78,8 @@ const DialogFooter = ({
)} )}
{...props} {...props}
/> />
) );
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, Dialog,
@ -118,5 +118,5 @@ export {
DialogHeader, DialogHeader,
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription
} };

View file

@ -1,27 +1,27 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react" import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
@ -36,9 +36,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ));
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@ -52,9 +52,9 @@ const DropdownMenuSubContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@ -71,13 +71,13 @@ const DropdownMenuContent = React.forwardRef<
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@ -89,8 +89,8 @@ const DropdownMenuItem = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@ -112,9 +112,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ));
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@ -135,13 +135,13 @@ const DropdownMenuRadioItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)) ));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
@ -153,8 +153,8 @@ const DropdownMenuLabel = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@ -165,8 +165,8 @@ const DropdownMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)) ));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({
className, className,
@ -177,9 +177,9 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)} className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props} {...props}
/> />
) );
} };
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {
DropdownMenu, DropdownMenu,
@ -196,5 +196,5 @@ export {
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuRadioGroup
} };

View file

@ -1,30 +1,30 @@
import * as React from "react" import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { import {
Controller, Controller,
ControllerProps, ControllerProps,
FieldPath, FieldPath,
FieldValues, FieldValues,
FormProvider, FormProvider,
useFormContext, useFormContext
} from "react-hook-form" } from "react-hook-form";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label";
const Form = FormProvider const Form = FormProvider;
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue
) );
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@ -36,21 +36,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>");
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@ -58,37 +58,37 @@ const useFormField = () => {
formItemId: `${id}-form-item`, formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue
) );
const FormItem = React.forwardRef< const FormItem = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider> </FormItemContext.Provider>
) );
}) });
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
@ -97,15 +97,16 @@ const FormLabel = React.forwardRef<
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
}) });
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef< const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>, React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot> React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return ( return (
<Slot <Slot
@ -119,15 +120,15 @@ const FormControl = React.forwardRef<
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
}) });
FormControl.displayName = "FormControl" FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
@ -136,19 +137,19 @@ const FormDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
) );
}) });
FormDescription.displayName = "FormDescription" FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef< const FormMessage = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children const body = error ? String(error?.message) : children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
@ -160,9 +161,9 @@ const FormMessage = React.forwardRef<
> >
{body} {body}
</p> </p>
) );
}) });
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage";
export { export {
useFormField, useFormField,
@ -172,5 +173,5 @@ export {
FormControl, FormControl,
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField
} };

View file

@ -29,7 +29,7 @@ export function GridPattern({
aria-hidden="true" aria-hidden="true"
className={cn( className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/35 stroke-gray-400/35", "pointer-events-none absolute inset-0 h-full w-full fill-gray-400/35 stroke-gray-400/35",
className, className
)} )}
{...props} {...props}
> >

View file

@ -6,151 +6,151 @@ import { AnimatePresence, motion } from "framer-motion";
import { Play, XIcon } from "lucide-react"; import { Play, XIcon } from "lucide-react";
type AnimationStyle = type AnimationStyle =
| "from-bottom" | "from-bottom"
| "from-center" | "from-center"
| "from-top" | "from-top"
| "from-left" | "from-left"
| "from-right" | "from-right"
| "fade" | "fade"
| "top-in-bottom-out" | "top-in-bottom-out"
| "left-in-right-out"; | "left-in-right-out";
interface HeroVideoProps { interface HeroVideoProps {
animationStyle?: AnimationStyle; animationStyle?: AnimationStyle;
videoSrc: string; videoSrc: string;
thumbnailSrc: string; thumbnailSrc: string;
thumbnailAlt?: string; thumbnailAlt?: string;
} }
const animationVariants = { const animationVariants = {
"from-bottom": { "from-bottom": {
initial: { y: "100%", opacity: 0 }, initial: { y: "100%", opacity: 0 },
animate: { y: 0, opacity: 1 }, animate: { y: 0, opacity: 1 },
exit: { y: "100%", opacity: 0 }, exit: { y: "100%", opacity: 0 }
}, },
"from-center": { "from-center": {
initial: { scale: 0.5, opacity: 0 }, initial: { scale: 0.5, opacity: 0 },
animate: { scale: 1, opacity: 1 }, animate: { scale: 1, opacity: 1 },
exit: { scale: 0.5, opacity: 0 }, exit: { scale: 0.5, opacity: 0 }
}, },
"from-top": { "from-top": {
initial: { y: "-100%", opacity: 0 }, initial: { y: "-100%", opacity: 0 },
animate: { y: 0, opacity: 1 }, animate: { y: 0, opacity: 1 },
exit: { y: "-100%", opacity: 0 }, exit: { y: "-100%", opacity: 0 }
}, },
"from-left": { "from-left": {
initial: { x: "-100%", opacity: 0 }, initial: { x: "-100%", opacity: 0 },
animate: { x: 0, opacity: 1 }, animate: { x: 0, opacity: 1 },
exit: { x: "-100%", opacity: 0 }, exit: { x: "-100%", opacity: 0 }
}, },
"from-right": { "from-right": {
initial: { x: "100%", opacity: 0 }, initial: { x: "100%", opacity: 0 },
animate: { x: 0, opacity: 1 }, animate: { x: 0, opacity: 1 },
exit: { x: "100%", opacity: 0 }, exit: { x: "100%", opacity: 0 }
}, },
fade: { fade: {
initial: { opacity: 0 }, initial: { opacity: 0 },
animate: { opacity: 1 }, animate: { opacity: 1 },
exit: { opacity: 0 }, exit: { opacity: 0 }
}, },
"top-in-bottom-out": { "top-in-bottom-out": {
initial: { y: "-100%", opacity: 0 }, initial: { y: "-100%", opacity: 0 },
animate: { y: 0, opacity: 1 }, animate: { y: 0, opacity: 1 },
exit: { y: "100%", opacity: 0 }, exit: { y: "100%", opacity: 0 }
}, },
"left-in-right-out": { "left-in-right-out": {
initial: { x: "-100%", opacity: 0 }, initial: { x: "-100%", opacity: 0 },
animate: { x: 0, opacity: 1 }, animate: { x: 0, opacity: 1 },
exit: { x: "100%", opacity: 0 }, exit: { x: "100%", opacity: 0 }
}, }
}; };
export default function HeroVideoDialog({ export default function HeroVideoDialog({
animationStyle = "from-center", animationStyle = "from-center",
videoSrc, videoSrc,
thumbnailSrc, thumbnailSrc,
thumbnailAlt = "Video thumbnail", thumbnailAlt = "Video thumbnail"
}: HeroVideoProps) { }: HeroVideoProps) {
const [isVideoOpen, setIsVideoOpen] = useState(false); const [isVideoOpen, setIsVideoOpen] = useState(false);
const [isCloseHovered, setIsCloseHovered] = useState(false); const [isCloseHovered, setIsCloseHovered] = useState(false);
const [isPlayHovered, setIsPlayHovered] = useState(false); const [isPlayHovered, setIsPlayHovered] = useState(false);
const openVideo = () => setIsVideoOpen(true); const openVideo = () => setIsVideoOpen(true);
const closeVideo = () => setIsVideoOpen(false); const closeVideo = () => setIsVideoOpen(false);
const selectedAnimation = animationVariants[animationStyle]; const selectedAnimation = animationVariants[animationStyle];
return ( return (
<div className="relative"> <div className="relative">
<div className="relative cursor-pointer" onClick={openVideo}> <div className="relative cursor-pointer" onClick={openVideo}>
<img <img
src={thumbnailSrc} src={thumbnailSrc}
alt={thumbnailAlt} alt={thumbnailAlt}
width={1920} width={1920}
height={1080} height={1080}
className="w-full rounded-2xl" className="w-full rounded-2xl"
/> />
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<div <div
className="border border-neutral-800 flex items-center justify-center rounded-full backdrop-blur-md transition-transform duration-300 ease-out size-24" className="border border-neutral-800 flex items-center justify-center rounded-full backdrop-blur-md transition-transform duration-300 ease-out size-24"
onMouseEnter={() => setIsPlayHovered(true)} onMouseEnter={() => setIsPlayHovered(true)}
onMouseLeave={() => setIsPlayHovered(false)} onMouseLeave={() => setIsPlayHovered(false)}
> >
<div <div
className={`flex items-center justify-center border border-neutral-800 rounded-full size-20 transition-all ease-out duration-300 backdrop-blur-2xl relative ${ className={`flex items-center justify-center border border-neutral-800 rounded-full size-20 transition-all ease-out duration-300 backdrop-blur-2xl relative ${
isPlayHovered ? " scale-105" : "scale-100" isPlayHovered ? " scale-105" : "scale-100"
}`} }`}
> >
<Play <Play
className="size-8 text-white" className="size-8 text-white"
style={{ style={{
transform: isPlayHovered ? "scale(1.1)" : "scale(1)", transform: isPlayHovered ? "scale(1.1)" : "scale(1)",
transition: "transform 0.3s ease", transition: "transform 0.3s ease"
}} }}
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<AnimatePresence> <AnimatePresence>
{isVideoOpen && ( {isVideoOpen && (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md" className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
> >
<motion.div <motion.div
{...selectedAnimation} {...selectedAnimation}
transition={{ type: "spring", damping: 30, stiffness: 300 }} transition={{ type: "spring", damping: 30, stiffness: 300 }}
className="relative w-full max-w-4xl aspect-video mx-4 md:mx-0" className="relative w-full max-w-4xl aspect-video mx-4 md:mx-0"
> >
<motion.button <motion.button
className="absolute -top-16 right-0 text-white text-xl bg-neutral-900/50 ring-1 backdrop-blur-md rounded-full p-2" className="absolute -top-16 right-0 text-white text-xl bg-neutral-900/50 ring-1 backdrop-blur-md rounded-full p-2"
onClick={closeVideo} onClick={closeVideo}
onHoverStart={() => setIsCloseHovered(true)} onHoverStart={() => setIsCloseHovered(true)}
onHoverEnd={() => setIsCloseHovered(false)} onHoverEnd={() => setIsCloseHovered(false)}
whileHover={{ scale: 1.1 }} whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
> >
<XIcon className="size-5" /> <XIcon className="size-5" />
</motion.button> </motion.button>
<motion.div <motion.div
className="size-full border-2 border-white rounded-2xl overflow-hidden isolate z-[1] relative" className="size-full border-2 border-white rounded-2xl overflow-hidden isolate z-[1] relative"
animate={{ scale: isCloseHovered ? 0.98 : 1 }} animate={{ scale: isCloseHovered ? 0.98 : 1 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
<iframe <iframe
src={videoSrc} src={videoSrc}
className="size-full rounded-2xl" className="size-full rounded-2xl"
allowFullScreen allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe> ></iframe>
</motion.div> </motion.div>
</motion.div> </motion.div>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
); );
} }

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