style: style the code according to Prettier rules
This commit is contained in:
parent
fe3ab6cdf1
commit
7bb89ce202
125 changed files with 5263 additions and 5263 deletions
|
@ -3,21 +3,21 @@ import Link from "next/link";
|
|||
import { FC } from "react";
|
||||
|
||||
interface CardProps {
|
||||
title: string;
|
||||
url: string;
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const Card: FC<CardProps> = ({ title, url }) => {
|
||||
return (
|
||||
<div className="bg-accent border rounded-lg hover:bg-muted transition-all">
|
||||
<Link href={url} className="group">
|
||||
<div className="flex-center rounded-lg p-6">
|
||||
<h2 className="text-2xl font-bold mb-2">{title}</h2>
|
||||
<ArrowUpRight className="w-5 h-5 mb-2 ml-2 opacity-0 group-hover:opacity-100 transition-all duration-300" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="bg-accent border rounded-lg hover:bg-muted transition-all">
|
||||
<Link href={url} className="group">
|
||||
<div className="flex-center rounded-lg p-6">
|
||||
<h2 className="text-2xl font-bold mb-2">{title}</h2>
|
||||
<ArrowUpRight className="w-5 h-5 mb-2 ml-2 opacity-0 group-hover:opacity-100 transition-all duration-300" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
||||
|
|
|
@ -9,49 +9,49 @@ import { Menu } from "lucide-react";
|
|||
import Logo from "@/components/shared/Logo";
|
||||
|
||||
const MobileNav = () => {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<header className="header">
|
||||
<Link href="/" className="flex items-center gap-2 md:py-2">
|
||||
<Logo width={120} height={40} />
|
||||
</Link>
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<header className="header">
|
||||
<Link href="/" className="flex items-center gap-2 md:py-2">
|
||||
<Logo width={120} height={40} />
|
||||
</Link>
|
||||
|
||||
<nav className="flex gap-2">
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<Menu className="w-5 h-5" />
|
||||
</SheetTrigger>
|
||||
<SheetContent className="sheet-content sm:w-64">
|
||||
<>
|
||||
<Logo width={155} height={53} />
|
||||
<ul className="header-nav_elements">
|
||||
{AdminLinks.slice(0, 6).map((link) => {
|
||||
const isActive = link.url === pathname;
|
||||
<nav className="flex gap-2">
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<Menu className="w-5 h-5" />
|
||||
</SheetTrigger>
|
||||
<SheetContent className="sheet-content sm:w-64">
|
||||
<>
|
||||
<Logo width={155} height={53} />
|
||||
<ul className="header-nav_elements">
|
||||
{AdminLinks.slice(0, 6).map((link) => {
|
||||
const isActive = link.url === pathname;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={link.url}
|
||||
className={`${
|
||||
isActive && "gradient-text"
|
||||
} p-18 flex whitespace-nowrap text-dark-700`}
|
||||
>
|
||||
<Link
|
||||
className="sidebar-link cursor-pointer"
|
||||
href={link.url}
|
||||
>
|
||||
<link.icon />
|
||||
{link.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
return (
|
||||
<li
|
||||
key={link.url}
|
||||
className={`${
|
||||
isActive && "gradient-text"
|
||||
} p-18 flex whitespace-nowrap text-dark-700`}
|
||||
>
|
||||
<Link
|
||||
className="sidebar-link cursor-pointer"
|
||||
href={link.url}
|
||||
>
|
||||
<link.icon />
|
||||
{link.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileNav;
|
||||
|
|
|
@ -6,60 +6,60 @@ import { AdminLinks } from "@/constants";
|
|||
import Logo from "@/components/shared/Logo";
|
||||
|
||||
const Sidebar = () => {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<>
|
||||
<aside className="sidebar">
|
||||
<div className="flex size-full flex-col gap-4">
|
||||
<Link href="/" className="sidebar-logo">
|
||||
<Logo width={155} height={53} />
|
||||
</Link>
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<>
|
||||
<aside className="sidebar">
|
||||
<div className="flex size-full flex-col gap-4">
|
||||
<Link href="/" className="sidebar-logo">
|
||||
<Logo width={155} height={53} />
|
||||
</Link>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
<ul className="sidebar-nav_elements">
|
||||
{AdminLinks.slice(0, 6).map((link) => {
|
||||
const isActive = link.url === pathname;
|
||||
<nav className="sidebar-nav">
|
||||
<ul className="sidebar-nav_elements">
|
||||
{AdminLinks.slice(0, 6).map((link) => {
|
||||
const isActive = link.url === pathname;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={link.url}
|
||||
className={`sidebar-nav_element group ${
|
||||
isActive ? "bg-white/5" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<Link className="sidebar-link" href={link.url}>
|
||||
<link.icon />
|
||||
{link.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
return (
|
||||
<li
|
||||
key={link.url}
|
||||
className={`sidebar-nav_element group ${
|
||||
isActive ? "bg-white/5" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<Link className="sidebar-link" href={link.url}>
|
||||
<link.icon />
|
||||
{link.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<ul className="sidebar-nav_elements">
|
||||
{AdminLinks.slice(6).map((link) => {
|
||||
const isActive = link.url === pathname;
|
||||
<ul className="sidebar-nav_elements">
|
||||
{AdminLinks.slice(6).map((link) => {
|
||||
const isActive = link.url === pathname;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={link.url}
|
||||
className={`sidebar-nav_element group ${
|
||||
isActive ? "bg-purple-gradient" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<Link className="sidebar-link" href={link.url}>
|
||||
<link.icon />
|
||||
{link.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<li
|
||||
key={link.url}
|
||||
className={`sidebar-nav_element group ${
|
||||
isActive ? "bg-purple-gradient" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<Link className="sidebar-link" href={link.url}>
|
||||
<link.icon />
|
||||
{link.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Admin // Changelogs",
|
||||
title: "Admin // Changelogs"
|
||||
};
|
||||
|
||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -5,21 +5,21 @@ import { useForm, SubmitHandler, useFieldArray } from "react-hook-form";
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { logsSchema } from "@/lib/validations/validation";
|
||||
|
@ -27,206 +27,206 @@ import { z } from "zod";
|
|||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
interface LogEntry {
|
||||
_id: string;
|
||||
version: string;
|
||||
date: string;
|
||||
bullets: { point: string }[];
|
||||
_id: string;
|
||||
version: string;
|
||||
date: string;
|
||||
bullets: { point: string }[];
|
||||
}
|
||||
|
||||
type LogsFormValues = z.infer<typeof logsSchema>;
|
||||
|
||||
const AdminLogPage = () => {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm<LogsFormValues>({
|
||||
resolver: zodResolver(logsSchema),
|
||||
defaultValues: {
|
||||
version: "",
|
||||
date: "",
|
||||
bullets: [{ point: "" }],
|
||||
},
|
||||
});
|
||||
const form = useForm<LogsFormValues>({
|
||||
resolver: zodResolver(logsSchema),
|
||||
defaultValues: {
|
||||
version: "",
|
||||
date: "",
|
||||
bullets: [{ point: "" }]
|
||||
}
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "bullets",
|
||||
});
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "bullets"
|
||||
});
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/logs", { method: "GET" });
|
||||
if (response.ok) {
|
||||
const data: LogEntry[] = await response.json();
|
||||
setLogs(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch logs");
|
||||
}
|
||||
};
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/logs", { method: "GET" });
|
||||
if (response.ok) {
|
||||
const data: LogEntry[] = await response.json();
|
||||
setLogs(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch logs");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
|
||||
setLoading(true);
|
||||
const response = await fetch("/api/uploadlogs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
|
||||
setLoading(true);
|
||||
const response = await fetch("/api/uploadlogs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
form.reset();
|
||||
fetchLogs();
|
||||
setLoading(false);
|
||||
toast({ description: "Logs successfully added" });
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast({ description: "Upload Failed", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
if (response.ok) {
|
||||
form.reset();
|
||||
fetchLogs();
|
||||
setLoading(false);
|
||||
toast({ description: "Logs successfully added" });
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast({ description: "Upload Failed", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteLog = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/delete/logs/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchLogs();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to delete log");
|
||||
}
|
||||
};
|
||||
const deleteLog = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/delete/logs/${id}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchLogs();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to delete log");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
const interval = setInterval(() => {
|
||||
fetchLogs();
|
||||
}, 10000);
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
const interval = setInterval(() => {
|
||||
fetchLogs();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="logs-page" className="wrapper container">
|
||||
<h1 className="text-3xl font-bold py-6">Server Logs Form</h1>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Released on 24 Nov 2024" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
return (
|
||||
<section id="logs-page" className="wrapper container">
|
||||
<h1 className="text-3xl font-bold py-6">Server Logs Form</h1>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Released on 24 Nov 2024" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.id}
|
||||
control={form.control}
|
||||
name={`bullets.${index}.point`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Key Point {index + 1}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant={"secondary"}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
className="mb-4"
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
onClick={() => append({ point: "" })}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
disabled={loading}
|
||||
size={"lg"}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.id}
|
||||
control={form.control}
|
||||
name={`bullets.${index}.point`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Key Point {index + 1}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant={"secondary"}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
className="mb-4"
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
onClick={() => append({ point: "" })}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
disabled={loading}
|
||||
size={"lg"}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{/* Section to list and delete logs */}
|
||||
<section id="logs-list" className="py-16 md:py-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Logs</h2>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Date</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{logs
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((log) => (
|
||||
<TableRow key={log._id}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{log.version}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{log.date}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deleteLog(log._id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
{/* Section to list and delete logs */}
|
||||
<section id="logs-list" className="py-16 md:py-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Logs</h2>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Date</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{logs
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((log) => (
|
||||
<TableRow key={log._id}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{log.version}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{log.date}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deleteLog(log._id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLogPage;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Admin // Downloads",
|
||||
title: "Admin // Downloads"
|
||||
};
|
||||
|
||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -6,246 +6,246 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { UploadButton } from "@/lib/uploadthing";
|
||||
import { downloadSchema } from "@/lib/validations/validation";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
|
||||
interface DownloadEntry {
|
||||
_id: string;
|
||||
fileName: string;
|
||||
version: string;
|
||||
downloadLink: string;
|
||||
fileSize: string;
|
||||
_id: string;
|
||||
fileName: string;
|
||||
version: string;
|
||||
downloadLink: string;
|
||||
fileSize: string;
|
||||
}
|
||||
|
||||
const DownloadsPage = () => {
|
||||
const { toast } = useToast();
|
||||
const [downloads, setDownloads] = useState<DownloadEntry[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const [downloads, setDownloads] = useState<DownloadEntry[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof downloadSchema>>({
|
||||
resolver: zodResolver(downloadSchema),
|
||||
defaultValues: {
|
||||
fileName: "",
|
||||
version: "",
|
||||
downloadLink: "",
|
||||
fileSize: "",
|
||||
},
|
||||
});
|
||||
const form = useForm<z.infer<typeof downloadSchema>>({
|
||||
resolver: zodResolver(downloadSchema),
|
||||
defaultValues: {
|
||||
fileName: "",
|
||||
version: "",
|
||||
downloadLink: "",
|
||||
fileSize: ""
|
||||
}
|
||||
});
|
||||
|
||||
const fetchDownloads = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/downloads", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: DownloadEntry[] = await response.json();
|
||||
setDownloads(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch downloads");
|
||||
}
|
||||
};
|
||||
const fetchDownloads = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/downloads", {
|
||||
method: "GET"
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: DownloadEntry[] = await response.json();
|
||||
setDownloads(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch downloads");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDownloads();
|
||||
const interval = setInterval(() => {
|
||||
fetchDownloads();
|
||||
}, 10000);
|
||||
useEffect(() => {
|
||||
fetchDownloads();
|
||||
const interval = setInterval(() => {
|
||||
fetchDownloads();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof downloadSchema>> = async (
|
||||
data
|
||||
) => {
|
||||
setLoading(true);
|
||||
const response = await fetch("/api/upload", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const onSubmit: SubmitHandler<z.infer<typeof downloadSchema>> = async (
|
||||
data
|
||||
) => {
|
||||
setLoading(true);
|
||||
const response = await fetch("/api/upload", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
form.reset();
|
||||
fetchDownloads();
|
||||
setLoading(false);
|
||||
toast({ description: "Download Successfully Updated" });
|
||||
} else {
|
||||
console.error("Upload failed");
|
||||
setLoading(false);
|
||||
toast({ description: "Uploading Failed", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
if (response.ok) {
|
||||
form.reset();
|
||||
fetchDownloads();
|
||||
setLoading(false);
|
||||
toast({ description: "Download Successfully Updated" });
|
||||
} else {
|
||||
console.error("Upload failed");
|
||||
setLoading(false);
|
||||
toast({ description: "Uploading Failed", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDownload = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/delete/downloads/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchDownloads();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to delete download");
|
||||
}
|
||||
};
|
||||
const deleteDownload = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/delete/downloads/${id}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchDownloads();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to delete download");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="downloads-page" className="wrapper container">
|
||||
<h1 className="text-3xl font-bold py-6">Downloads Form</h1>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="downloadLink"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Download Link</FormLabel>
|
||||
<UploadButton
|
||||
endpoint="imageUploader"
|
||||
onClientUploadComplete={(res) => {
|
||||
field.onChange(res[0].url);
|
||||
}}
|
||||
onUploadError={(error: Error) => {
|
||||
alert(`ERROR! ${error.message}`);
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Size</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
size={"lg"}
|
||||
disabled={loading}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
return (
|
||||
<section id="downloads-page" className="wrapper container">
|
||||
<h1 className="text-3xl font-bold py-6">Downloads Form</h1>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="downloadLink"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Download Link</FormLabel>
|
||||
<UploadButton
|
||||
endpoint="imageUploader"
|
||||
onClientUploadComplete={(res) => {
|
||||
field.onChange(res[0].url);
|
||||
}}
|
||||
onUploadError={(error: Error) => {
|
||||
alert(`ERROR! ${error.message}`);
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Size</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
size={"lg"}
|
||||
disabled={loading}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{/* Section to list and delete downloads */}
|
||||
<section id="downloads-list" className="py-16 md:py-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Downloads</h2>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">
|
||||
Download Link
|
||||
</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{downloads
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((download) => (
|
||||
<TableRow key={download._id}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{download.fileName}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{download.version}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<a
|
||||
href={download.downloadLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{download.downloadLink}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{download.fileSize}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deleteDownload(download._id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
{/* Section to list and delete downloads */}
|
||||
<section id="downloads-list" className="py-16 md:py-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Downloads</h2>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">
|
||||
Download Link
|
||||
</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{downloads
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((download) => (
|
||||
<TableRow key={download._id}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{download.fileName}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{download.version}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<a
|
||||
href={download.downloadLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{download.downloadLink}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{download.fileSize}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deleteDownload(download._id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadsPage;
|
||||
|
|
|
@ -4,130 +4,130 @@ import { useState, useEffect } from "react";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious
|
||||
} from "@/components/ui/pagination";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Subscriber {
|
||||
email: string;
|
||||
subscribedAt: string;
|
||||
email: string;
|
||||
subscribedAt: string;
|
||||
}
|
||||
|
||||
const EmailPage = () => {
|
||||
const [subscribers, setSubscribers] = useState<Subscriber[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const { toast } = useToast();
|
||||
const [subscribers, setSubscribers] = useState<Subscriber[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
// Function to fetch subscribers data
|
||||
const fetchSubscribers = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/newsletter/subscriber?page=${currentPage}`
|
||||
);
|
||||
const data = await res.json();
|
||||
setSubscribers(data.subscribers);
|
||||
setTotalPages(data.totalPages);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error fetching subscribers",
|
||||
description: `${error}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
// Function to fetch subscribers data
|
||||
const fetchSubscribers = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/newsletter/subscriber?page=${currentPage}`
|
||||
);
|
||||
const data = await res.json();
|
||||
setSubscribers(data.subscribers);
|
||||
setTotalPages(data.totalPages);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error fetching subscribers",
|
||||
description: `${error}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch data initially
|
||||
fetchSubscribers();
|
||||
// Fetch data initially
|
||||
fetchSubscribers();
|
||||
|
||||
// Set up interval to fetch data every 10 seconds
|
||||
const intervalId = setInterval(fetchSubscribers, 10000);
|
||||
// Set up interval to fetch data every 10 seconds
|
||||
const intervalId = setInterval(fetchSubscribers, 10000);
|
||||
|
||||
// Clear interval on component unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}, [currentPage, toast]);
|
||||
// Clear interval on component unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}, [currentPage, toast]);
|
||||
|
||||
return (
|
||||
<section id="downloads-page" className="wrapper container">
|
||||
<h1 className="text-3xl md:text-4xl font-bold py-6">Newsletter Emails</h1>
|
||||
<Link href="/email-editor">
|
||||
<Button>Create a new email</Button>
|
||||
</Link>
|
||||
<section id="downloads-list" className="py-8">
|
||||
<h2 className="text-2xl font-semibold">Newsletter Subscribers</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Total subscribers: {subscribers.length}
|
||||
</p>
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">Email</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">
|
||||
Subscribed At
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{subscribers.map((subscriber, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{subscriber.email}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{new Date(subscriber.subscribedAt).toLocaleDateString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex-center mt-12">
|
||||
{totalPages > 1 && (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
{currentPage > 1 && (
|
||||
<PaginationPrevious
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
/>
|
||||
)}
|
||||
</PaginationItem>
|
||||
{Array.from({ length: totalPages }).map((_, i) => (
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
isActive={currentPage === i + 1}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
{currentPage < totalPages && (
|
||||
<PaginationNext
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
/>
|
||||
)}
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section id="downloads-page" className="wrapper container">
|
||||
<h1 className="text-3xl md:text-4xl font-bold py-6">Newsletter Emails</h1>
|
||||
<Link href="/email-editor">
|
||||
<Button>Create a new email</Button>
|
||||
</Link>
|
||||
<section id="downloads-list" className="py-8">
|
||||
<h2 className="text-2xl font-semibold">Newsletter Subscribers</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Total subscribers: {subscribers.length}
|
||||
</p>
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">Email</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">
|
||||
Subscribed At
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{subscribers.map((subscriber, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{subscriber.email}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{new Date(subscriber.subscribedAt).toLocaleDateString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex-center mt-12">
|
||||
{totalPages > 1 && (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
{currentPage > 1 && (
|
||||
<PaginationPrevious
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
/>
|
||||
)}
|
||||
</PaginationItem>
|
||||
{Array.from({ length: totalPages }).map((_, i) => (
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
isActive={currentPage === i + 1}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
{currentPage < totalPages && (
|
||||
<PaginationNext
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
/>
|
||||
)}
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailPage;
|
||||
|
|
|
@ -2,15 +2,15 @@ import MobileNav from "../_components/Mobilenav";
|
|||
import Sidebar from "../_components/Sidebar";
|
||||
|
||||
export default function PageLayout({
|
||||
children,
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<main className="flex flex-col min-h-screen root">
|
||||
<Sidebar />
|
||||
<MobileNav />
|
||||
<div className="root-container lg:px-24">{children}</div>
|
||||
</main>
|
||||
);
|
||||
return (
|
||||
<main className="flex flex-col min-h-screen root">
|
||||
<Sidebar />
|
||||
<MobileNav />
|
||||
<div className="root-container lg:px-24">{children}</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Admin // Mods",
|
||||
title: "Admin // Mods"
|
||||
};
|
||||
|
||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -6,401 +6,401 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { UploadButton } from "@/lib/uploadthing";
|
||||
import { modsSchema } from "@/lib/validations/validation";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface ModEntry {
|
||||
_id: string;
|
||||
fileName: string;
|
||||
version: string;
|
||||
downloadLink: string;
|
||||
fileSize: string;
|
||||
_id: string;
|
||||
fileName: string;
|
||||
version: string;
|
||||
downloadLink: string;
|
||||
fileSize: string;
|
||||
}
|
||||
|
||||
const SvrjsModsAdminPage = () => {
|
||||
const { toast } = useToast();
|
||||
const [mods, setMods] = useState<ModEntry[]>([]);
|
||||
const [editMod, setEditMod] = useState<ModEntry | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const [mods, setMods] = useState<ModEntry[]>([]);
|
||||
const [editMod, setEditMod] = useState<ModEntry | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const mainForm = useForm<z.infer<typeof modsSchema>>({
|
||||
resolver: zodResolver(modsSchema),
|
||||
defaultValues: {
|
||||
fileName: "",
|
||||
version: "",
|
||||
downloadLink: "",
|
||||
fileSize: "",
|
||||
},
|
||||
});
|
||||
const mainForm = useForm<z.infer<typeof modsSchema>>({
|
||||
resolver: zodResolver(modsSchema),
|
||||
defaultValues: {
|
||||
fileName: "",
|
||||
version: "",
|
||||
downloadLink: "",
|
||||
fileSize: ""
|
||||
}
|
||||
});
|
||||
|
||||
const dialogForm = useForm<z.infer<typeof modsSchema>>({
|
||||
resolver: zodResolver(modsSchema),
|
||||
defaultValues: {
|
||||
fileName: "",
|
||||
version: "",
|
||||
downloadLink: "",
|
||||
fileSize: "",
|
||||
},
|
||||
});
|
||||
const dialogForm = useForm<z.infer<typeof modsSchema>>({
|
||||
resolver: zodResolver(modsSchema),
|
||||
defaultValues: {
|
||||
fileName: "",
|
||||
version: "",
|
||||
downloadLink: "",
|
||||
fileSize: ""
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchMods();
|
||||
const interval = setInterval(() => {
|
||||
fetchMods();
|
||||
}, 10000);
|
||||
useEffect(() => {
|
||||
fetchMods();
|
||||
const interval = setInterval(() => {
|
||||
fetchMods();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (editMod) {
|
||||
dialogForm.reset({
|
||||
fileName: editMod.fileName,
|
||||
version: editMod.version,
|
||||
downloadLink: editMod.downloadLink,
|
||||
fileSize: editMod.fileSize,
|
||||
});
|
||||
setDialogOpen(true); // Open dialog when a mod is being edited
|
||||
}
|
||||
}, [editMod]);
|
||||
useEffect(() => {
|
||||
if (editMod) {
|
||||
dialogForm.reset({
|
||||
fileName: editMod.fileName,
|
||||
version: editMod.version,
|
||||
downloadLink: editMod.downloadLink,
|
||||
fileSize: editMod.fileSize
|
||||
});
|
||||
setDialogOpen(true); // Open dialog when a mod is being edited
|
||||
}
|
||||
}, [editMod]);
|
||||
|
||||
const fetchMods = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/mods", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: ModEntry[] = await response.json();
|
||||
setMods(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch mods");
|
||||
}
|
||||
};
|
||||
const fetchMods = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/mods", {
|
||||
method: "GET"
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: ModEntry[] = await response.json();
|
||||
setMods(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch mods");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof modsSchema>> = async (data) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = editMod
|
||||
? await fetch(`/api/update/mods/${editMod._id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
: await fetch("/api/uploadmods", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const onSubmit: SubmitHandler<z.infer<typeof modsSchema>> = async (data) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = editMod
|
||||
? await fetch(`/api/update/mods/${editMod._id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
: await fetch("/api/uploadmods", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
mainForm.reset();
|
||||
dialogForm.reset();
|
||||
fetchMods();
|
||||
setLoading(false);
|
||||
setEditMod(null);
|
||||
setDialogOpen(false); // Close dialog on successful submission
|
||||
toast({
|
||||
description: "Successfully Saved Changes",
|
||||
});
|
||||
} else {
|
||||
console.error("Save failed");
|
||||
setLoading(false);
|
||||
toast({
|
||||
description: "Save failed",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Save failed", error);
|
||||
setLoading(false);
|
||||
toast({
|
||||
description: "Save failed",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
if (response.ok) {
|
||||
mainForm.reset();
|
||||
dialogForm.reset();
|
||||
fetchMods();
|
||||
setLoading(false);
|
||||
setEditMod(null);
|
||||
setDialogOpen(false); // Close dialog on successful submission
|
||||
toast({
|
||||
description: "Successfully Saved Changes"
|
||||
});
|
||||
} else {
|
||||
console.error("Save failed");
|
||||
setLoading(false);
|
||||
toast({
|
||||
description: "Save failed",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Save failed", error);
|
||||
setLoading(false);
|
||||
toast({
|
||||
description: "Save failed",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMod = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/delete/mods/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchMods();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to delete mod");
|
||||
}
|
||||
};
|
||||
const deleteMod = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/delete/mods/${id}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchMods();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to delete mod");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="mods-page" className="wrapper container">
|
||||
<h1 className="text-3xl font-bold py-6">Mods Form</h1>
|
||||
<Form {...mainForm}>
|
||||
<form onSubmit={mainForm.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={mainForm.control}
|
||||
name="fileName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={mainForm.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={mainForm.control}
|
||||
name="downloadLink"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Download Link</FormLabel>
|
||||
<UploadButton
|
||||
endpoint="imageUploader"
|
||||
onClientUploadComplete={(res) => {
|
||||
field.onChange(res[0].url);
|
||||
}}
|
||||
onUploadError={(error: Error) => {
|
||||
alert(`ERROR! ${error.message}`);
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={mainForm.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Size</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
size={"lg"}
|
||||
disabled={loading}
|
||||
>
|
||||
{editMod ? "Save Changes" : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
return (
|
||||
<section id="mods-page" className="wrapper container">
|
||||
<h1 className="text-3xl font-bold py-6">Mods Form</h1>
|
||||
<Form {...mainForm}>
|
||||
<form onSubmit={mainForm.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={mainForm.control}
|
||||
name="fileName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={mainForm.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={mainForm.control}
|
||||
name="downloadLink"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Download Link</FormLabel>
|
||||
<UploadButton
|
||||
endpoint="imageUploader"
|
||||
onClientUploadComplete={(res) => {
|
||||
field.onChange(res[0].url);
|
||||
}}
|
||||
onUploadError={(error: Error) => {
|
||||
alert(`ERROR! ${error.message}`);
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={mainForm.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Size</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
size={"lg"}
|
||||
disabled={loading}
|
||||
>
|
||||
{editMod ? "Save Changes" : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{/* Section to list and delete mods */}
|
||||
<section id="mods-list" className="py-16 md:py-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Mods</h2>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">
|
||||
Download Link
|
||||
</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mods
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((mod) => (
|
||||
<TableRow key={mod._id}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{mod.fileName}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{mod.version}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<a
|
||||
href={mod.downloadLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{mod.downloadLink}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{mod.fileSize}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2 gap-2 flex-center">
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditMod(mod)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>Edit Content</DialogTitle>
|
||||
{/* Section to list and delete mods */}
|
||||
<section id="mods-list" className="py-16 md:py-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Mods</h2>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">
|
||||
Download Link
|
||||
</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mods
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((mod) => (
|
||||
<TableRow key={mod._id}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{mod.fileName}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{mod.version}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<a
|
||||
href={mod.downloadLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{mod.downloadLink}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{mod.fileSize}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2 gap-2 flex-center">
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditMod(mod)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>Edit Content</DialogTitle>
|
||||
|
||||
<Form {...dialogForm}>
|
||||
<form
|
||||
onSubmit={dialogForm.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={dialogForm.control}
|
||||
name="fileName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={editMod?.fileName}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={dialogForm.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={editMod?.version}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={dialogForm.control}
|
||||
name="downloadLink"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Download Link</FormLabel>
|
||||
<UploadButton
|
||||
endpoint="imageUploader"
|
||||
onClientUploadComplete={(res) => {
|
||||
field.onChange(res[0].url);
|
||||
}}
|
||||
onUploadError={(error: Error) => {
|
||||
alert(`ERROR! ${error.message}`);
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={editMod?.downloadLink}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={dialogForm.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Size</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={editMod?.fileSize}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
size={"lg"}
|
||||
disabled={loading}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deleteMod(mod._id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
<Form {...dialogForm}>
|
||||
<form
|
||||
onSubmit={dialogForm.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={dialogForm.control}
|
||||
name="fileName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={editMod?.fileName}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={dialogForm.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={editMod?.version}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={dialogForm.control}
|
||||
name="downloadLink"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Download Link</FormLabel>
|
||||
<UploadButton
|
||||
endpoint="imageUploader"
|
||||
onClientUploadComplete={(res) => {
|
||||
field.onChange(res[0].url);
|
||||
}}
|
||||
onUploadError={(error: Error) => {
|
||||
alert(`ERROR! ${error.message}`);
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={editMod?.downloadLink}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={dialogForm.control}
|
||||
name="fileSize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Size</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
defaultValue={editMod?.fileSize}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
size={"lg"}
|
||||
disabled={loading}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deleteMod(mod._id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SvrjsModsAdminPage;
|
||||
|
|
|
@ -7,79 +7,79 @@ import { Button } from "@/components/ui/button";
|
|||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), {
|
||||
ssr: false,
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const EditPage = ({ params }: { params: { slug: string } }) => {
|
||||
const router = useRouter();
|
||||
const { slug } = params;
|
||||
const { toast } = useToast();
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [vulnerabilities, setVulnerabilities] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const { slug } = params;
|
||||
const { toast } = useToast();
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [vulnerabilities, setVulnerabilities] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
fetch(`/api/mdx/pages/${slug}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setTitle(data.title);
|
||||
setContent(data.content);
|
||||
setVulnerabilities(data.vulnerabilities || "");
|
||||
})
|
||||
.catch((error) => console.error("Failed to load page", error));
|
||||
}
|
||||
}, [slug]);
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
fetch(`/api/mdx/pages/${slug}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setTitle(data.title);
|
||||
setContent(data.content);
|
||||
setVulnerabilities(data.vulnerabilities || "");
|
||||
})
|
||||
.catch((error) => console.error("Failed to load page", error));
|
||||
}
|
||||
}, [slug]);
|
||||
|
||||
const savePage = async () => {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title, content, vulnerabilities }),
|
||||
});
|
||||
const savePage = async () => {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title, content, vulnerabilities })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setLoading(false);
|
||||
toast({ description: "Page successfully updated" });
|
||||
router.push(`/admin/multi-logs/`);
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast({ description: "Page Updated" });
|
||||
}
|
||||
};
|
||||
if (response.ok) {
|
||||
setLoading(false);
|
||||
toast({ description: "Page successfully updated" });
|
||||
router.push(`/admin/multi-logs/`);
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast({ description: "Page Updated" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditorChange = (value?: string) => {
|
||||
if (value !== undefined) {
|
||||
setContent(value);
|
||||
}
|
||||
};
|
||||
const handleEditorChange = (value?: string) => {
|
||||
if (value !== undefined) {
|
||||
setContent(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="edit-page" className="wrapper container gap-4">
|
||||
<h1 className="text-3xl font-bold py-6">Edit Page: {slug}</h1>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Page Title"
|
||||
/>
|
||||
<MarkdownEditor
|
||||
value={content}
|
||||
onChange={handleEditorChange}
|
||||
height={560}
|
||||
/>
|
||||
<h1 className="text-3xl font-bold py-6">Vulnerabilities</h1>
|
||||
<MarkdownEditor
|
||||
value={vulnerabilities}
|
||||
onChange={(value) => setVulnerabilities(value || "")}
|
||||
height={200}
|
||||
/>
|
||||
<Button onClick={savePage} disabled={loading} className="mt-4">
|
||||
Save
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section id="edit-page" className="wrapper container gap-4">
|
||||
<h1 className="text-3xl font-bold py-6">Edit Page: {slug}</h1>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Page Title"
|
||||
/>
|
||||
<MarkdownEditor
|
||||
value={content}
|
||||
onChange={handleEditorChange}
|
||||
height={560}
|
||||
/>
|
||||
<h1 className="text-3xl font-bold py-6">Vulnerabilities</h1>
|
||||
<MarkdownEditor
|
||||
value={vulnerabilities}
|
||||
onChange={(value) => setVulnerabilities(value || "")}
|
||||
height={200}
|
||||
/>
|
||||
<Button onClick={savePage} disabled={loading} className="mt-4">
|
||||
Save
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPage;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Admin // MultiLogs",
|
||||
title: "Admin // MultiLogs"
|
||||
};
|
||||
|
||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -2,157 +2,157 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface PageEntry {
|
||||
title: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MultiLogs = () => {
|
||||
const [pages, setPages] = useState<PageEntry[]>([]);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pageTitle, setPageTitle] = useState("");
|
||||
const [pages, setPages] = useState<PageEntry[]>([]);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pageTitle, setPageTitle] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/mdx/pages")
|
||||
.then((response) => response.json())
|
||||
.then((data) => setPages(data))
|
||||
.catch((error) => console.error("Failed to load pages", error));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fetch("/api/mdx/pages")
|
||||
.then((response) => response.json())
|
||||
.then((data) => setPages(data))
|
||||
.catch((error) => console.error("Failed to load pages", error));
|
||||
}, []);
|
||||
|
||||
const createPage = async () => {
|
||||
setLoading(true);
|
||||
const slug = pageTitle.toLowerCase().replace(/\s+/g, "-");
|
||||
const response = await fetch("/api/mdx/pages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: pageTitle, slug, content: "" }),
|
||||
});
|
||||
const createPage = async () => {
|
||||
setLoading(true);
|
||||
const slug = pageTitle.toLowerCase().replace(/\s+/g, "-");
|
||||
const response = await fetch("/api/mdx/pages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: pageTitle, slug, content: "" })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const newPage = await response.json();
|
||||
setPages([...pages, newPage]);
|
||||
setPageTitle("");
|
||||
setOpen(false);
|
||||
router.push(`/admin/multi-logs/${slug}`);
|
||||
toast({ description: "Page created successfully" });
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error("Failed to create page:", errorData);
|
||||
toast({ description: `Error: ${errorData.message}` });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
if (response.ok) {
|
||||
const newPage = await response.json();
|
||||
setPages([...pages, newPage]);
|
||||
setPageTitle("");
|
||||
setOpen(false);
|
||||
router.push(`/admin/multi-logs/${slug}`);
|
||||
toast({ description: "Page created successfully" });
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error("Failed to create page:", errorData);
|
||||
toast({ description: `Error: ${errorData.message}` });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const deletePage = async (slug: string) => {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
const deletePage = async (slug: string) => {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setPages(pages.filter((page) => page.slug !== slug));
|
||||
toast({ description: "Page deleted successfully" });
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error("Failed to delete page:", errorData);
|
||||
toast({ description: `Error: ${errorData.message}` });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
if (response.ok) {
|
||||
setPages(pages.filter((page) => page.slug !== slug));
|
||||
toast({ description: "Page deleted successfully" });
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error("Failed to delete page:", errorData);
|
||||
toast({ description: `Error: ${errorData.message}` });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="logs-page" className="wrapper container">
|
||||
<section id="create-page" className="py-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-2">Create New Page</h2>
|
||||
<Button variant={"secondary"} onClick={() => setOpen(true)}>
|
||||
Create New Page
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Enter Page Title</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={pageTitle}
|
||||
onChange={(e) => setPageTitle(e.target.value)}
|
||||
placeholder="Page Title"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button disabled={loading} onClick={createPage}>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
<section id="pages-list" className="pb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Pages</h2>
|
||||
<p className="mb-4">Total Pages: {pages.length}</p>
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">Slug</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pages.map((page) => (
|
||||
<TableRow key={page.slug}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<a
|
||||
href={`/changelogs/${page.slug}`}
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
{page.slug}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() =>
|
||||
router.push(`/admin/multi-logs/${page.slug}`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deletePage(page.slug)}
|
||||
className="ml-2"
|
||||
disabled={loading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section id="logs-page" className="wrapper container">
|
||||
<section id="create-page" className="py-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-2">Create New Page</h2>
|
||||
<Button variant={"secondary"} onClick={() => setOpen(true)}>
|
||||
Create New Page
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Enter Page Title</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={pageTitle}
|
||||
onChange={(e) => setPageTitle(e.target.value)}
|
||||
placeholder="Page Title"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button disabled={loading} onClick={createPage}>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
<section id="pages-list" className="pb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Pages</h2>
|
||||
<p className="mb-4">Total Pages: {pages.length}</p>
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">Slug</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pages.map((page) => (
|
||||
<TableRow key={page.slug}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<a
|
||||
href={`/changelogs/${page.slug}`}
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
{page.slug}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() =>
|
||||
router.push(`/admin/multi-logs/${page.slug}`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deletePage(page.slug)}
|
||||
className="ml-2"
|
||||
disabled={loading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiLogs;
|
||||
|
|
|
@ -3,19 +3,19 @@ import Card from "../_components/Card";
|
|||
import { AdminDashboardLINKS } from "@/constants";
|
||||
|
||||
const AdminPage = () => {
|
||||
return (
|
||||
<>
|
||||
<section id="adminpage" className="wrapper container">
|
||||
<h1 className="h2-bold py-6">Admin Page</h1>
|
||||
return (
|
||||
<>
|
||||
<section id="adminpage" className="wrapper container">
|
||||
<h1 className="h2-bold py-6">Admin Page</h1>
|
||||
|
||||
<div className="grid lg:grid-cols-2 grid-cols-1 gap-4 ">
|
||||
{AdminDashboardLINKS.map((item, idx) => (
|
||||
<Card key={idx} title={item.label} url={item.url} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
<div className="grid lg:grid-cols-2 grid-cols-1 gap-4 ">
|
||||
{AdminDashboardLINKS.map((item, idx) => (
|
||||
<Card key={idx} title={item.label} url={item.url} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPage;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Admin // Vulnerabilities",
|
||||
title: "Admin // Vulnerabilities"
|
||||
};
|
||||
|
||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -5,217 +5,217 @@ import { useForm, SubmitHandler, useFieldArray } from "react-hook-form";
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { vulnerabilitiesSchema } from "@/lib/validations/validation";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger
|
||||
} from "@/components/ui/select";
|
||||
|
||||
interface VulnerabiltyEntry {
|
||||
_id: string;
|
||||
version: string;
|
||||
bullets: { point: string }[];
|
||||
_id: string;
|
||||
version: string;
|
||||
bullets: { point: string }[];
|
||||
}
|
||||
|
||||
type VulnerabiltiesForm = z.infer<typeof vulnerabilitiesSchema>;
|
||||
|
||||
const AdminLogPage = () => {
|
||||
const [logs, setLogs] = useState<VulnerabiltyEntry[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<VulnerabiltyEntry[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm<VulnerabiltiesForm>({
|
||||
resolver: zodResolver(vulnerabilitiesSchema),
|
||||
defaultValues: {
|
||||
version: "",
|
||||
bullets: [{ point: "" }],
|
||||
},
|
||||
});
|
||||
const form = useForm<VulnerabiltiesForm>({
|
||||
resolver: zodResolver(vulnerabilitiesSchema),
|
||||
defaultValues: {
|
||||
version: "",
|
||||
bullets: [{ point: "" }]
|
||||
}
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "bullets",
|
||||
});
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "bullets"
|
||||
});
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/vulnerabilities", { method: "GET" });
|
||||
if (response.ok) {
|
||||
const data: VulnerabiltyEntry[] = await response.json();
|
||||
setLogs(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch logs");
|
||||
}
|
||||
};
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/vulnerabilities", { method: "GET" });
|
||||
if (response.ok) {
|
||||
const data: VulnerabiltyEntry[] = await response.json();
|
||||
setLogs(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch logs");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<VulnerabiltiesForm> = async (data) => {
|
||||
setLoading(true);
|
||||
const response = await fetch("/api/uploadvulnerabilities", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const onSubmit: SubmitHandler<VulnerabiltiesForm> = async (data) => {
|
||||
setLoading(true);
|
||||
const response = await fetch("/api/uploadvulnerabilities", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
form.reset();
|
||||
fetchLogs();
|
||||
setLoading(false);
|
||||
toast({ description: "Logs successfully added" });
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast({ description: "Upload Failed", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
if (response.ok) {
|
||||
form.reset();
|
||||
fetchLogs();
|
||||
setLoading(false);
|
||||
toast({ description: "Logs successfully added" });
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast({ description: "Upload Failed", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteLog = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/delete/vulnerability/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchLogs();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to delete log");
|
||||
}
|
||||
};
|
||||
const deleteLog = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/delete/vulnerability/${id}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchLogs();
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to delete log");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
const interval = setInterval(() => {
|
||||
fetchLogs();
|
||||
}, 10000);
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
const interval = setInterval(() => {
|
||||
fetchLogs();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="logs-page" className="wrapper container">
|
||||
<h1 className="text-3xl font-bold py-6">Server Vulnerabilties Form</h1>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
return (
|
||||
<section id="logs-page" className="wrapper container">
|
||||
<h1 className="text-3xl font-bold py-6">Server Vulnerabilties Form</h1>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.id}
|
||||
control={form.control}
|
||||
name={`bullets.${index}.point`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Key Point {index + 1}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant={"secondary"}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
className="mb-4"
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
onClick={() => append({ point: "" })}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
disabled={loading}
|
||||
size={"lg"}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.id}
|
||||
control={form.control}
|
||||
name={`bullets.${index}.point`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Key Point {index + 1}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant={"secondary"}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
className="mb-4"
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
onClick={() => append({ point: "" })}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full text-lg rounded-full"
|
||||
disabled={loading}
|
||||
size={"lg"}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{/* Section to list and delete logs */}
|
||||
<section id="logs-list" className="py-16 md:py-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">
|
||||
Existing Vulnerabilties
|
||||
</h2>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{logs
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((log) => (
|
||||
<TableRow key={log._id}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{log.version}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deleteLog(log._id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
{/* Section to list and delete logs */}
|
||||
<section id="logs-list" className="py-16 md:py-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">
|
||||
Existing Vulnerabilties
|
||||
</h2>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table className="w-full mt-4 border-muted">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{logs
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((log) => (
|
||||
<TableRow key={log._id}>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
{log.version}
|
||||
</TableCell>
|
||||
<TableCell className="border-b px-4 py-2">
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => deleteLog(log._id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLogPage;
|
||||
|
|
|
@ -8,140 +8,140 @@ import { EXAMPLE_A1 } from "@/constants";
|
|||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
const EmailEditor = () => {
|
||||
const { toast } = useToast();
|
||||
const [subject, setSubject] = useState("");
|
||||
const [previewContent, setPreviewContent] = useState<string>(EXAMPLE_A1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const [subject, setSubject] = useState("");
|
||||
const [previewContent, setPreviewContent] = useState<string>(EXAMPLE_A1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const validateInputs = () => {
|
||||
if (!subject.trim() || !previewContent.trim()) {
|
||||
toast({
|
||||
title: "Validation Error",
|
||||
description: "Subject and content cannot be empty.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const validateInputs = () => {
|
||||
if (!subject.trim() || !previewContent.trim()) {
|
||||
toast({
|
||||
title: "Validation Error",
|
||||
description: "Subject and content cannot be empty.",
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSendAll = async () => {
|
||||
if (!validateInputs()) return;
|
||||
const handleSendAll = async () => {
|
||||
if (!validateInputs()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/newsletter/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject: subject,
|
||||
html: previewContent,
|
||||
}),
|
||||
});
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/newsletter/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject: subject,
|
||||
html: previewContent
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
toast({
|
||||
title: "Success!",
|
||||
description: result.message || "Emails sent successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
toast({
|
||||
title: "Uh oh!",
|
||||
description: `Failed to send emails: ${error}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const result = await response.json();
|
||||
toast({
|
||||
title: "Success!",
|
||||
description: result.message || "Emails sent successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
toast({
|
||||
title: "Uh oh!",
|
||||
description: `Failed to send emails: ${error}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendTest = async () => {
|
||||
if (!validateInputs()) return;
|
||||
const handleSendTest = async () => {
|
||||
if (!validateInputs()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/newsletter/test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject: subject,
|
||||
html: previewContent,
|
||||
}),
|
||||
});
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/newsletter/test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject: subject,
|
||||
html: previewContent
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
toast({
|
||||
title: "Success!",
|
||||
description: result.message || "Test email sent successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
toast({
|
||||
title: "Uh oh!",
|
||||
description: `Failed to send test email: ${error}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const result = await response.json();
|
||||
toast({
|
||||
title: "Success!",
|
||||
description: result.message || "Test email sent successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
toast({
|
||||
title: "Uh oh!",
|
||||
description: `Failed to send test email: ${error}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditorChange = (value: string) => {
|
||||
setPreviewContent(value);
|
||||
};
|
||||
const handleEditorChange = (value: string) => {
|
||||
setPreviewContent(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/admin/email" className="text-blue-500 underline">
|
||||
Back
|
||||
</Link>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Subject"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
className="border rounded-md p-2"
|
||||
/>
|
||||
<CodeEditor onChange={handleEditorChange} />
|
||||
<div className="flex space-x-2 mt-4">
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={handleSendTest}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Sending..." : "Send Test"}
|
||||
</Button>
|
||||
<Button onClick={handleSendAll} disabled={loading}>
|
||||
{loading ? "Sending..." : "Send All"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<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">
|
||||
<Link href="/admin/email" className="text-blue-500 underline">
|
||||
Back
|
||||
</Link>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Subject"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
className="border rounded-md p-2"
|
||||
/>
|
||||
<CodeEditor onChange={handleEditorChange} />
|
||||
<div className="flex space-x-2 mt-4">
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={handleSendTest}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Sending..." : "Send Test"}
|
||||
</Button>
|
||||
<Button onClick={handleSendAll} disabled={loading}>
|
||||
{loading ? "Sending..." : "Send All"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-1/2 p-4 overflow-auto">
|
||||
<h2 className="text-2xl font-bold mb-4 text-secondary-foreground">
|
||||
Email Preview
|
||||
</h2>
|
||||
<div
|
||||
className="border rounded-md p-4"
|
||||
dangerouslySetInnerHTML={{ __html: previewContent }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="w-full lg:w-1/2 p-4 overflow-auto">
|
||||
<h2 className="text-2xl font-bold mb-4 text-secondary-foreground">
|
||||
Email Preview
|
||||
</h2>
|
||||
<div
|
||||
className="border rounded-md p-4"
|
||||
dangerouslySetInnerHTML={{ __html: previewContent }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailEditor;
|
||||
|
|
|
@ -2,13 +2,9 @@ import React from "react";
|
|||
import AuthProvider from "../../components/shared/providers/AuthProvider";
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
);
|
||||
return <AuthProvider>{children}</AuthProvider>;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import PrismLoader from "@/components/loader/prismLoader";
|
|||
import { Button } from "@/components/ui/button";
|
||||
|
||||
async function getData(slug: string) {
|
||||
const query = `
|
||||
const query = `
|
||||
*[_type == "blog" && slug.current == '${slug}'] {
|
||||
"currentSlug": slug.current,
|
||||
title,
|
||||
|
@ -28,177 +28,177 @@ async function getData(slug: string) {
|
|||
_createdAt
|
||||
}[0]`;
|
||||
|
||||
const data = await client.fetch(query);
|
||||
return data;
|
||||
const data = await client.fetch(query);
|
||||
return data;
|
||||
}
|
||||
|
||||
interface BlogSlugArticle {
|
||||
currentSlug: string;
|
||||
title: string;
|
||||
content: any;
|
||||
titleImage: string;
|
||||
_createdAt: string;
|
||||
currentSlug: string;
|
||||
title: string;
|
||||
content: any;
|
||||
titleImage: string;
|
||||
_createdAt: string;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
params
|
||||
}: {
|
||||
params: { slug: string };
|
||||
params: { slug: string };
|
||||
}): Promise<Metadata> {
|
||||
const data = await getData(params.slug);
|
||||
const data = await getData(params.slug);
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
title: "Not Found",
|
||||
description: "Blog post not found",
|
||||
};
|
||||
}
|
||||
if (!data) {
|
||||
return {
|
||||
title: "Not Found",
|
||||
description: "Blog post not found"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${data.title} - SVRJS`,
|
||||
description: data.smallDescription,
|
||||
openGraph: {
|
||||
title: `${data.title} - SVRJS`,
|
||||
description: data.smallDescription,
|
||||
url: `https://svrjs.org/blog/${data.currentSlug}`,
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: urlFor(data.titleImage).url(),
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: `${data.title} - SVRJS`,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: `${data.title} - SVRJS`,
|
||||
description: data.smallDescription,
|
||||
images: [urlFor(data.titleImage).url()],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
};
|
||||
return {
|
||||
title: `${data.title} - SVRJS`,
|
||||
description: data.smallDescription,
|
||||
openGraph: {
|
||||
title: `${data.title} - SVRJS`,
|
||||
description: data.smallDescription,
|
||||
url: `https://svrjs.org/blog/${data.currentSlug}`,
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: urlFor(data.titleImage).url(),
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: `${data.title} - SVRJS`
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: `${data.title} - SVRJS`,
|
||||
description: data.smallDescription,
|
||||
images: [urlFor(data.titleImage).url()],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const customPortableTextComponents: PortableTextComponents = {
|
||||
types: {
|
||||
image: ({ value }) => {
|
||||
return (
|
||||
<div className="my-8">
|
||||
<Image
|
||||
src={urlFor(value).url()}
|
||||
alt={value.alt || "Blog Image"}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="w-full h-auto rounded-lg"
|
||||
/>
|
||||
{value.caption && (
|
||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
||||
{value.caption}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
code: ({ value }) => {
|
||||
const language = value.language || "javascript";
|
||||
const grammar = Prism.languages[language];
|
||||
types: {
|
||||
image: ({ value }) => {
|
||||
return (
|
||||
<div className="my-8">
|
||||
<Image
|
||||
src={urlFor(value).url()}
|
||||
alt={value.alt || "Blog Image"}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="w-full h-auto rounded-lg"
|
||||
/>
|
||||
{value.caption && (
|
||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
||||
{value.caption}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
code: ({ value }) => {
|
||||
const language = value.language || "javascript";
|
||||
const grammar = Prism.languages[language];
|
||||
|
||||
if (!grammar) {
|
||||
console.error(`No grammar found for language: "${language}"`);
|
||||
return (
|
||||
<pre className="p-4 rounded-md overflow-x-auto text-sm">
|
||||
<code>{value.code}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
if (!grammar) {
|
||||
console.error(`No grammar found for language: "${language}"`);
|
||||
return (
|
||||
<pre className="p-4 rounded-md overflow-x-auto text-sm">
|
||||
<code>{value.code}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative my-8">
|
||||
<pre
|
||||
className={`language-${language} p-4 rounded-md overflow-x-auto text-sm`}
|
||||
>
|
||||
<code className={`language-${language}`}>{value.code}</code>
|
||||
</pre>
|
||||
<PrismLoader />
|
||||
<CopyButton code={value.code} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
return (
|
||||
<div className="relative my-8">
|
||||
<pre
|
||||
className={`language-${language} p-4 rounded-md overflow-x-auto text-sm`}
|
||||
>
|
||||
<code className={`language-${language}`}>{value.code}</code>
|
||||
</pre>
|
||||
<PrismLoader />
|
||||
<CopyButton code={value.code} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
notFound();
|
||||
}
|
||||
if (!data) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formattedDate = format(new Date(data._createdAt), "MMMM d, yyyy");
|
||||
const formattedDate = format(new Date(data._createdAt), "MMMM d, yyyy");
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="group text-primary transition-all flex items-center"
|
||||
>
|
||||
<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" />
|
||||
Back
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href="/rss.xml"
|
||||
className="ml-auto"
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
>
|
||||
<Button
|
||||
variant={"link"}
|
||||
size={"lg"}
|
||||
className="mx-0 px-2 text-accent-foreground"
|
||||
>
|
||||
<Rss className="w-5 h-5 mr-1" /> Subscribe to RSS
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<header className="text-start mb-8 w-full">
|
||||
{data.titleImage && (
|
||||
<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">
|
||||
{data.title}
|
||||
</h1>
|
||||
<Image
|
||||
src={urlFor(data.titleImage).url()}
|
||||
alt={data.title}
|
||||
width={1200}
|
||||
height={800}
|
||||
priority
|
||||
className="w-full h-auto object-cover rounded-md"
|
||||
/>
|
||||
<p className="mt-4 text-lg md:text-xl text-muted-foreground">
|
||||
Uploaded at {formattedDate}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<Separator className="mb-6" />
|
||||
<article className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<PortableText
|
||||
value={data.content}
|
||||
components={customPortableTextComponents}
|
||||
/>
|
||||
</article>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="group text-primary transition-all flex items-center"
|
||||
>
|
||||
<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" />
|
||||
Back
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href="/rss.xml"
|
||||
className="ml-auto"
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
>
|
||||
<Button
|
||||
variant={"link"}
|
||||
size={"lg"}
|
||||
className="mx-0 px-2 text-accent-foreground"
|
||||
>
|
||||
<Rss className="w-5 h-5 mr-1" /> Subscribe to RSS
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<header className="text-start mb-8 w-full">
|
||||
{data.titleImage && (
|
||||
<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">
|
||||
{data.title}
|
||||
</h1>
|
||||
<Image
|
||||
src={urlFor(data.titleImage).url()}
|
||||
alt={data.title}
|
||||
width={1200}
|
||||
height={800}
|
||||
priority
|
||||
className="w-full h-auto object-cover rounded-md"
|
||||
/>
|
||||
<p className="mt-4 text-lg md:text-xl text-muted-foreground">
|
||||
Uploaded at {formattedDate}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<Separator className="mb-6" />
|
||||
<article className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<PortableText
|
||||
value={data.content}
|
||||
components={customPortableTextComponents}
|
||||
/>
|
||||
</article>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,61 +6,61 @@ import { Button } from "@/components/ui/button";
|
|||
import Link from "next/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Blog - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/blog",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Blog - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Blog - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "Blog - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Blog - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/blog",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Blog - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Blog - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<section
|
||||
id="blog"
|
||||
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">
|
||||
SVRJS Blog Post
|
||||
</h1>
|
||||
<p className="text-muted-foreground flex-center mb-2">
|
||||
Stay updated with our latest blog posts by subscribing to our
|
||||
<Link href="/rss.xml" rel="alternate" type="application/rss+xml">
|
||||
<Button variant={"link"} className="mx-0 px-2">
|
||||
<Rss className="w-5 h-5 mr-1" /> RSS feed
|
||||
</Button>
|
||||
</Link>
|
||||
</p>
|
||||
<BlogCards searchParams={searchParams} />
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section
|
||||
id="blog"
|
||||
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">
|
||||
SVRJS Blog Post
|
||||
</h1>
|
||||
<p className="text-muted-foreground flex-center mb-2">
|
||||
Stay updated with our latest blog posts by subscribing to our
|
||||
<Link href="/rss.xml" rel="alternate" type="application/rss+xml">
|
||||
<Button variant={"link"} className="mx-0 px-2">
|
||||
<Rss className="w-5 h-5 mr-1" /> RSS feed
|
||||
</Button>
|
||||
</Link>
|
||||
</p>
|
||||
<BlogCards searchParams={searchParams} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogPage;
|
||||
|
|
|
@ -5,126 +5,126 @@ import ReactMarkdown from "react-markdown";
|
|||
import Head from "next/head";
|
||||
|
||||
const Page = ({ params }: { params: { slug: string } }) => {
|
||||
const { slug } = params;
|
||||
const [page, setPage] = useState<{ title: string; content: string } | null>(
|
||||
null
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const { slug } = params;
|
||||
const [page, setPage] = useState<{ title: string; content: string } | null>(
|
||||
null
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/mdx/pages/${slug}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setPage(data);
|
||||
return (document.title = `${data.title} Change Log - SVRJS`);
|
||||
} else {
|
||||
if (response.status === 404) {
|
||||
setNotFound(true);
|
||||
return (document.title = "404 Not Found");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load page", error);
|
||||
setNotFound(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/mdx/pages/${slug}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setPage(data);
|
||||
return (document.title = `${data.title} Change Log - SVRJS`);
|
||||
} else {
|
||||
if (response.status === 404) {
|
||||
setNotFound(true);
|
||||
return (document.title = "404 Not Found");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load page", error);
|
||||
setNotFound(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPage();
|
||||
}, [slug]);
|
||||
fetchPage();
|
||||
}, [slug]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<head>
|
||||
<title>Mods Change Logs - SVRJS</title>
|
||||
</head>
|
||||
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||
<div className="mb-3">
|
||||
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<head>
|
||||
<title>Mods Change Logs - SVRJS</title>
|
||||
</head>
|
||||
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||
<div className="mb-3">
|
||||
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (notFound) {
|
||||
return (
|
||||
<section id="404error" className="flex-center flex-col wrapper container">
|
||||
<h1 className="text-3xl md:text-5xl text-center">
|
||||
<span className="text-red-500">404</span> Page not Found
|
||||
</h1>
|
||||
<p className="text-lg mt-3 text-muted-foreground">
|
||||
Please return back to Home
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
if (notFound) {
|
||||
return (
|
||||
<section id="404error" className="flex-center flex-col wrapper container">
|
||||
<h1 className="text-3xl md:text-5xl text-center">
|
||||
<span className="text-red-500">404</span> Page not Found
|
||||
</h1>
|
||||
<p className="text-lg mt-3 text-muted-foreground">
|
||||
Please return back to Home
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<head>
|
||||
<title>{page.title} Changelog - SVRJS</title>
|
||||
<meta
|
||||
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.`}
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
return (
|
||||
<>
|
||||
<head>
|
||||
<title>{page.title} Changelog - SVRJS</title>
|
||||
<meta
|
||||
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.`}
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<meta property="og:title" content={`${page.title} Changelog - SVRJS`} />
|
||||
<meta
|
||||
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.`}
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content={`https://svrjs.org/changelogs/${slug}`}
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://svrjs.vercel.app/metadata/svrjs-cover.png"
|
||||
/>
|
||||
<title>Documentation - SVRJS</title>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={`${page.title} Changelog - SVRJS`}
|
||||
/>
|
||||
<meta
|
||||
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.`}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://svrjs.vercel.app/metadata/svrjs-cover.png"
|
||||
/>
|
||||
</head>
|
||||
<meta property="og:title" content={`${page.title} Changelog - SVRJS`} />
|
||||
<meta
|
||||
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.`}
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content={`https://svrjs.org/changelogs/${slug}`}
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://svrjs.vercel.app/metadata/svrjs-cover.png"
|
||||
/>
|
||||
<title>Documentation - SVRJS</title>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={`${page.title} Changelog - SVRJS`}
|
||||
/>
|
||||
<meta
|
||||
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.`}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://svrjs.vercel.app/metadata/svrjs-cover.png"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<section className="wrapper container py-24 md:py-28 gap-2 flex flex-col">
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
{page.title} Change Log
|
||||
</h1>
|
||||
<ReactMarkdown className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
{page.content}
|
||||
</ReactMarkdown>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
<section className="wrapper container py-24 md:py-28 gap-2 flex flex-col">
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
{page.title} Change Log
|
||||
</h1>
|
||||
<ReactMarkdown className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
{page.content}
|
||||
</ReactMarkdown>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -2,36 +2,36 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "ChangeLogs - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "ChangeLogs - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/changelogs",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "ChangeLogs - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "ChangeLogs - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "ChangeLogs - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "ChangeLogs - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/changelogs",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "ChangeLogs - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "ChangeLogs - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
const ContactLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <main>{children}</main>;
|
||||
return <main>{children}</main>;
|
||||
};
|
||||
|
||||
export default ContactLayout;
|
||||
|
|
|
@ -9,103 +9,103 @@ import { CHANGE_LOGS } from "@/constants/guidelines";
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
interface Bullet {
|
||||
point: string;
|
||||
point: string;
|
||||
}
|
||||
|
||||
interface LOGS {
|
||||
_id: string;
|
||||
date: string;
|
||||
version: string;
|
||||
bullets?: Bullet[]; // Make bullets optional
|
||||
_id: string;
|
||||
date: string;
|
||||
version: string;
|
||||
bullets?: Bullet[]; // Make bullets optional
|
||||
}
|
||||
|
||||
const LogsPage: React.FC = () => {
|
||||
const [downloads, setDownloads] = useState<LOGS[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [downloads, setDownloads] = useState<LOGS[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchDownloads = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/logs", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: LOGS[] = await response.json();
|
||||
setDownloads(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch downloads");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const fetchDownloads = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/logs", {
|
||||
method: "GET"
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: LOGS[] = await response.json();
|
||||
setDownloads(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch downloads");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDownloads();
|
||||
useEffect(() => {
|
||||
fetchDownloads();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDownloads();
|
||||
}, 10000);
|
||||
const interval = setInterval(() => {
|
||||
fetchDownloads();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
const reversedDownloads = [...downloads].reverse();
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
const reversedDownloads = [...downloads].reverse();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<head>
|
||||
<title>Change Logs - SVRJS</title>
|
||||
</head>
|
||||
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||
<div className="mb-3">
|
||||
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<head>
|
||||
<title>Change Logs - SVRJS</title>
|
||||
</head>
|
||||
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||
<div className="mb-3">
|
||||
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id="logs"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Server LOGS
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Get all the latest version of SVRJS download and compiled Files here!
|
||||
</p>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
return (
|
||||
<section
|
||||
id="logs"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Server LOGS
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Get all the latest version of SVRJS download and compiled Files here!
|
||||
</p>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
|
||||
{reversedDownloads.map((download) => (
|
||||
<div
|
||||
key={download._id}
|
||||
className="flex-start prose max-w-full md:prose-lg dark:prose-invert flex-col mb-4"
|
||||
>
|
||||
<h2 className="font-bold text-3xl">{download.version}</h2>
|
||||
<span className="font-medium italic">{download.date}</span>
|
||||
<ul className="list-disc pl-5">
|
||||
{(download.bullets ?? []).map((bullet, index) => (
|
||||
<li key={index}>{bullet.point}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<ReactMarkdown>{CHANGE_LOGS}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
{reversedDownloads.map((download) => (
|
||||
<div
|
||||
key={download._id}
|
||||
className="flex-start prose max-w-full md:prose-lg dark:prose-invert flex-col mb-4"
|
||||
>
|
||||
<h2 className="font-bold text-3xl">{download.version}</h2>
|
||||
<span className="font-medium italic">{download.date}</span>
|
||||
<ul className="list-disc pl-5">
|
||||
{(download.bullets ?? []).map((bullet, index) => (
|
||||
<li key={index}>{bullet.point}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<ReactMarkdown>{CHANGE_LOGS}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogsPage;
|
||||
|
|
|
@ -2,36 +2,36 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "Contact Us - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Contact Us - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/contact",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Contact Us - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Contact Us - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "Contact Us - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Contact Us - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/contact",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Contact Us - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Contact Us - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
const ContactLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <main>{children}</main>;
|
||||
return <main>{children}</main>;
|
||||
};
|
||||
|
||||
export default ContactLayout;
|
||||
|
|
|
@ -7,12 +7,12 @@ import { z } from "zod";
|
|||
import { contactFormSchema } from "@/lib/validations/validation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
@ -23,179 +23,179 @@ import { emails } from "@/constants";
|
|||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
const ContactUs = () => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||
|
||||
const form = useForm<z.infer<typeof contactFormSchema>>({
|
||||
resolver: zodResolver(contactFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
},
|
||||
});
|
||||
const form = useForm<z.infer<typeof contactFormSchema>>({
|
||||
resolver: zodResolver(contactFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
message: ""
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
||||
if (!captchaToken) {
|
||||
setShowCaptcha(true);
|
||||
return;
|
||||
}
|
||||
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
||||
if (!captchaToken) {
|
||||
setShowCaptcha(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ...values, captchaToken }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ...values, captchaToken }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
form.reset();
|
||||
setCaptchaToken(null); // Reset captcha token after successful submission
|
||||
toast({
|
||||
description: "Your message has been sent.",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Uh oh! Something went wrong.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
title: "Uh oh! Something went wrong.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowCaptcha(false); // Hide captcha after submission attempt
|
||||
}
|
||||
}
|
||||
if (res.ok) {
|
||||
form.reset();
|
||||
setCaptchaToken(null); // Reset captcha token after successful submission
|
||||
toast({
|
||||
description: "Your message has been sent."
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Uh oh! Something went wrong.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
title: "Uh oh! Something went wrong.",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowCaptcha(false); // Hide captcha after submission attempt
|
||||
}
|
||||
}
|
||||
|
||||
function handleCaptchaVerify(token: string) {
|
||||
setCaptchaToken(token);
|
||||
onSubmit(form.getValues()); // Trigger form submission after captcha is verified
|
||||
}
|
||||
function handleCaptchaVerify(token: string) {
|
||||
setCaptchaToken(token);
|
||||
onSubmit(form.getValues()); // Trigger form submission after captcha is verified
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300">
|
||||
<h1 className="text-4xl md:text-6xl tracking-tight font-bold uppercase text-center text-gray-900 dark:text-white">
|
||||
Contact Us
|
||||
</h1>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300">
|
||||
<h1 className="text-4xl md:text-6xl tracking-tight font-bold uppercase text-center text-gray-900 dark:text-white">
|
||||
Contact Us
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<section id="contact" className="w-full">
|
||||
<div className="flex-center flex-col md:flex-row justify-between mx-auto p-6 max-w-5xl">
|
||||
{/* Left contact page */}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4 pb-8 mb-8 max-w-lg w-full bg-accent border p-6 rounded-lg shadow-md"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="Your Email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Message</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-44"
|
||||
placeholder="Your Message"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<section id="contact" className="w-full">
|
||||
<div className="flex-center flex-col md:flex-row justify-between mx-auto p-6 max-w-5xl">
|
||||
{/* Left contact page */}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4 pb-8 mb-8 max-w-lg w-full bg-accent border p-6 rounded-lg shadow-md"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="Your Email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Message</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-44"
|
||||
placeholder="Your Message"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{showCaptcha && (
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
onVerify={handleCaptchaVerify}
|
||||
/>
|
||||
)}
|
||||
{showCaptcha && (
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
onVerify={handleCaptchaVerify}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"default"}
|
||||
className="w-full mt-2"
|
||||
disabled={loading}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="tracking-tight font-semibold">SEND</span>
|
||||
<Send className="ml-2 w-5 h-5" />
|
||||
</div>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"default"}
|
||||
className="w-full mt-2"
|
||||
disabled={loading}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="tracking-tight font-semibold">SEND</span>
|
||||
<Send className="ml-2 w-5 h-5" />
|
||||
</div>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{/* Right contact page */}
|
||||
<div className="max-w-lg mt-8 md:mt-0 md:ml-8 p-12 border rounded-lg">
|
||||
<ul className="space-y-4 mb-6">
|
||||
{emails.map((email, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="text-gray-600 dark:text-gray-300 flex items-center"
|
||||
>
|
||||
<email.icon className="mr-2" size={24} />
|
||||
<span>
|
||||
<a
|
||||
href={email.url}
|
||||
title={`Send an email to ${email.email}`}
|
||||
className="text-muted-foreground hover:text-accent-foreground transition duration-200"
|
||||
>
|
||||
{email.email}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Separator />
|
||||
<ul className="flex justify-center space-x-3 my-6">
|
||||
<Iconss />
|
||||
</ul>
|
||||
<Separator />
|
||||
<div className="text-center text-gray-500 mt-2 text-sm font-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
{/* Right contact page */}
|
||||
<div className="max-w-lg mt-8 md:mt-0 md:ml-8 p-12 border rounded-lg">
|
||||
<ul className="space-y-4 mb-6">
|
||||
{emails.map((email, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="text-gray-600 dark:text-gray-300 flex items-center"
|
||||
>
|
||||
<email.icon className="mr-2" size={24} />
|
||||
<span>
|
||||
<a
|
||||
href={email.url}
|
||||
title={`Send an email to ${email.email}`}
|
||||
className="text-muted-foreground hover:text-accent-foreground transition duration-200"
|
||||
>
|
||||
{email.email}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Separator />
|
||||
<ul className="flex justify-center space-x-3 my-6">
|
||||
<Iconss />
|
||||
</ul>
|
||||
<Separator />
|
||||
<div className="text-center text-gray-500 mt-2 text-sm font-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactUs;
|
||||
|
|
|
@ -4,52 +4,52 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "Contribute - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Contribute - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/contribute",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Contribute - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Contribute - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "Contribute - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Contribute - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/contribute",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Contribute - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Contribute - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
const Contribute = () => {
|
||||
return (
|
||||
<section
|
||||
id="tos"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Contributing to SVR.JS
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
We welcome contributions from the community! Here's how you can
|
||||
help!
|
||||
</p>
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<ReactMarkdown>{contribute}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section
|
||||
id="tos"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Contributing to SVR.JS
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
We welcome contributions from the community! Here's how you can
|
||||
help!
|
||||
</p>
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<ReactMarkdown>{contribute}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contribute;
|
||||
|
|
|
@ -2,39 +2,39 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "Downloads - SVRJS",
|
||||
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!",
|
||||
openGraph: {
|
||||
title: "Downloads - SVRJS",
|
||||
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!",
|
||||
url: "https://svrjs.org/downloads",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Downloads - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Downloads - SVRJS",
|
||||
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!",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "Downloads - SVRJS",
|
||||
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!",
|
||||
openGraph: {
|
||||
title: "Downloads - SVRJS",
|
||||
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!",
|
||||
url: "https://svrjs.org/downloads",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Downloads - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Downloads - SVRJS",
|
||||
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!",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
|
||||
export default function DownloadLayout({
|
||||
children,
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <main>{children}</main>;
|
||||
return <main>{children}</main>;
|
||||
}
|
||||
|
|
|
@ -3,111 +3,111 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Download } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
interface Download {
|
||||
_id: string;
|
||||
date: string;
|
||||
fileName: string;
|
||||
version: string;
|
||||
fileSize: string;
|
||||
downloadLink?: string; // Optional
|
||||
_id: string;
|
||||
date: string;
|
||||
fileName: string;
|
||||
version: string;
|
||||
fileSize: string;
|
||||
downloadLink?: string; // Optional
|
||||
}
|
||||
|
||||
const DownloadPage: React.FC = () => {
|
||||
const [downloads, setDownloads] = useState<Download[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [downloads, setDownloads] = useState<Download[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const fetchDownloads = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/downloads", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: Download[] = await response.json();
|
||||
setDownloads(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message);
|
||||
}
|
||||
};
|
||||
const fetchDownloads = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/downloads", {
|
||||
method: "GET"
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: Download[] = await response.json();
|
||||
setDownloads(data);
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDownloads();
|
||||
useEffect(() => {
|
||||
fetchDownloads();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDownloads();
|
||||
}, 10000);
|
||||
const interval = setInterval(() => {
|
||||
fetchDownloads();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="download"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Downloads
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Get all the latest version of SVRJS download and compiled Files here!
|
||||
</p>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table>
|
||||
<TableCaption>A list of all available downloads.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[150px]">Date</TableHead>
|
||||
<TableHead>File Name</TableHead>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead>File Size</TableHead>
|
||||
<TableHead className="text-right">Download Link</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{downloads
|
||||
.slice(0, 10)
|
||||
.reverse()
|
||||
.map((download) => (
|
||||
<TableRow key={download._id}>
|
||||
<TableCell className="font-medium">{download.date}</TableCell>
|
||||
<TableCell>{download.fileName}</TableCell>
|
||||
<TableCell>{download.version}</TableCell>
|
||||
<TableCell className="text-left">{download.fileSize}</TableCell>
|
||||
<TableCell className="flex items-center justify-end">
|
||||
{download.downloadLink ? (
|
||||
<Link href={download.downloadLink}>
|
||||
<Button variant={"ghost"} className="">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Button variant={"ghost"} disabled>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Unavailable
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section
|
||||
id="download"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Downloads
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Get all the latest version of SVRJS download and compiled Files here!
|
||||
</p>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<Table>
|
||||
<TableCaption>A list of all available downloads.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[150px]">Date</TableHead>
|
||||
<TableHead>File Name</TableHead>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead>File Size</TableHead>
|
||||
<TableHead className="text-right">Download Link</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{downloads
|
||||
.slice(0, 10)
|
||||
.reverse()
|
||||
.map((download) => (
|
||||
<TableRow key={download._id}>
|
||||
<TableCell className="font-medium">{download.date}</TableCell>
|
||||
<TableCell>{download.fileName}</TableCell>
|
||||
<TableCell>{download.version}</TableCell>
|
||||
<TableCell className="text-left">{download.fileSize}</TableCell>
|
||||
<TableCell className="flex items-center justify-end">
|
||||
{download.downloadLink ? (
|
||||
<Link href={download.downloadLink}>
|
||||
<Button variant={"ghost"} className="">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Button variant={"ghost"} disabled>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Unavailable
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadPage;
|
||||
|
|
|
@ -3,11 +3,11 @@ import React from "react";
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Forum - SVRJS",
|
||||
title: "Forum - SVRJS"
|
||||
};
|
||||
|
||||
const Forum = () => {
|
||||
return <div>Forum</div>;
|
||||
return <div>Forum</div>;
|
||||
};
|
||||
|
||||
export default Forum;
|
||||
|
|
|
@ -6,54 +6,54 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
openGraph: {
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
url: "https://svrjs.org",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "SVRJS - A Web Server running on Node.js",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
openGraph: {
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
url: "https://svrjs.org",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "SVRJS - A Web Server running on Node.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
|
||||
export default function PageLayout({
|
||||
children,
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const iconClassName = "w-4 h-4 flex-center text-zinc-950 -mr-2";
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
{/* Comment or edit this whenever required */}
|
||||
<Banner
|
||||
icon={<Home className={iconClassName} />}
|
||||
title="SVR.JS 4.0.0 is now on beta!"
|
||||
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/"
|
||||
buttonText="Read more"
|
||||
/>
|
||||
<Navbar />
|
||||
<div className="flex-grow flex-1 overflow-x-hidden">{children}</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
const iconClassName = "w-4 h-4 flex-center text-zinc-950 -mr-2";
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
{/* Comment or edit this whenever required */}
|
||||
<Banner
|
||||
icon={<Home className={iconClassName} />}
|
||||
title="SVR.JS 4.0.0 is now on beta!"
|
||||
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/"
|
||||
buttonText="Read more"
|
||||
/>
|
||||
<Navbar />
|
||||
<div className="flex-grow flex-1 overflow-x-hidden">{children}</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,36 +2,36 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "Mods - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Mods - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/mods",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Mods - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Mods - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "Mods - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Mods - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/mods",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Mods - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Mods - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
const ModLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <main>{children}</main>;
|
||||
return <main>{children}</main>;
|
||||
};
|
||||
|
||||
export default ModLayout;
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Download } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
@ -30,7 +30,7 @@ const ModsPage: React.FC = () => {
|
|||
const fetchDownloads = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/mods", {
|
||||
method: "GET",
|
||||
method: "GET"
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: Mods[] = await response.json();
|
||||
|
|
|
@ -2,7 +2,7 @@ import Newsletter from "@/components/shared/Newsletter";
|
|||
import React from "react";
|
||||
|
||||
const NewsletterPage = () => {
|
||||
return <Newsletter />;
|
||||
return <Newsletter />;
|
||||
};
|
||||
|
||||
export default NewsletterPage;
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import Link from "next/link";
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<section id="404error" className="flex-center flex-col wrapper container">
|
||||
<h1 className="text-3xl md:text-5xl text-center">
|
||||
<span className="text-red-500">404</span> Page not Found
|
||||
</h1>
|
||||
<p className="text-lg mt-3 text-muted-foreground">
|
||||
Please return back to{" "}
|
||||
<Link href="/" className="underline font-bold">
|
||||
Home
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section id="404error" className="flex-center flex-col wrapper container">
|
||||
<h1 className="text-3xl md:text-5xl text-center">
|
||||
<span className="text-red-500">404</span> Page not Found
|
||||
</h1>
|
||||
<p className="text-lg mt-3 text-muted-foreground">
|
||||
Please return back to{" "}
|
||||
<Link href="/" className="underline font-bold">
|
||||
Home
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
|
|
|
@ -8,18 +8,18 @@ import Partners from "@/components/shared/Partners";
|
|||
import Testimonials from "@/components/shared/Testimonials";
|
||||
|
||||
const RootPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<HowItWorks />
|
||||
<Testimonials />
|
||||
<Partners />
|
||||
<About />
|
||||
{/* <DataTable /> */}
|
||||
<Faq />
|
||||
<Newsletter />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<HowItWorks />
|
||||
<Testimonials />
|
||||
<Partners />
|
||||
<About />
|
||||
{/* <DataTable /> */}
|
||||
<Faq />
|
||||
<Newsletter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootPage;
|
||||
|
|
|
@ -6,52 +6,52 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy Policy - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Privacy Policy - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/privacy-policy",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Privacy Policy - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Privacy Policy - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "Privacy Policy - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Privacy Policy - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/privacy-policy",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Privacy Policy - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Privacy Policy - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
|
||||
const PrivacyPolicy = () => {
|
||||
return (
|
||||
<section
|
||||
id="privacy-policy"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Privacy Policy
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Effective date: 26.05.2024
|
||||
</p>
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<ReactMarkdown>{PRIVACY_POLICY}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section
|
||||
id="privacy-policy"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Privacy Policy
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Effective date: 26.05.2024
|
||||
</p>
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<ReactMarkdown>{PRIVACY_POLICY}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPolicy;
|
||||
|
|
|
@ -4,52 +4,52 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "Terms of Service - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Terms of Service - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/tos",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Terms of Service - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Terms of Service - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "Terms of Service - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Terms of Service - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/tos",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Terms of Service - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Terms of Service - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
|
||||
const TermsOfService = () => {
|
||||
return (
|
||||
<section
|
||||
id="tos"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Terms Of Service
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Last updated: 24.04.2024
|
||||
</p>
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<ReactMarkdown>{TERMS_AND_CONDITIONS}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section
|
||||
id="tos"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Terms Of Service
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Last updated: 24.04.2024
|
||||
</p>
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||
<ReactMarkdown>{TERMS_AND_CONDITIONS}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsOfService;
|
||||
|
|
|
@ -2,36 +2,36 @@ import { Metadata } from "next";
|
|||
|
||||
// baseURL [ENV]
|
||||
export const metadata: Metadata = {
|
||||
title: "Vulnerabilities - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Vulnerabilities - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/vulnerabilities",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Vulnerabilities - SVRJS",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Vulnerabilities - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "Vulnerabilities - SVRJS",
|
||||
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.",
|
||||
openGraph: {
|
||||
title: "Vulnerabilities - SVRJS",
|
||||
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.",
|
||||
url: "https://svrjs.org/vulnerabilities",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "Vulnerabilities - SVRJS"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "Vulnerabilities - SVRJS",
|
||||
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.",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
const ModLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <main>{children}</main>;
|
||||
return <main>{children}</main>;
|
||||
};
|
||||
|
||||
export default ModLayout;
|
||||
|
|
|
@ -6,159 +6,159 @@ import { useEffect, useState } from "react";
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
interface Bullet {
|
||||
point: string;
|
||||
point: string;
|
||||
}
|
||||
|
||||
interface Vulnerabilities {
|
||||
_id: string;
|
||||
version: string;
|
||||
bullets?: Bullet[]; // Make bullets optional
|
||||
_id: string;
|
||||
version: string;
|
||||
bullets?: Bullet[]; // Make bullets optional
|
||||
}
|
||||
|
||||
interface ModsVulnerability {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
vulnerabilities: string;
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
vulnerabilities: string;
|
||||
}
|
||||
|
||||
const Vulnerabilities = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [downloads, setDownloads] = useState<Vulnerabilities[]>([]);
|
||||
const [mods, setMods] = useState<ModsVulnerability[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [downloads, setDownloads] = useState<Vulnerabilities[]>([]);
|
||||
const [mods, setMods] = useState<ModsVulnerability[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/vulnerabilities", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: Vulnerabilities[] = await response.json();
|
||||
setDownloads(data);
|
||||
return (document.title = "Vulnerabilities - SVRJS");
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch downloads");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/vulnerabilities", {
|
||||
method: "GET"
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: Vulnerabilities[] = await response.json();
|
||||
setDownloads(data);
|
||||
return (document.title = "Vulnerabilities - SVRJS");
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch downloads");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMods = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/mdx/pages`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: ModsVulnerability[] = await response.json();
|
||||
// Filter out entries where vulnerabilities is undefined or an empty string
|
||||
const filteredMods = data.filter(
|
||||
(mod) => mod.vulnerabilities && mod.vulnerabilities.trim() !== ""
|
||||
);
|
||||
setMods(filteredMods);
|
||||
return (document.title = "Vulnerabilities - SVRJS");
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch vulnerabilities");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const fetchMods = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/mdx/pages`, {
|
||||
method: "GET"
|
||||
});
|
||||
if (response.ok) {
|
||||
const data: ModsVulnerability[] = await response.json();
|
||||
// Filter out entries where vulnerabilities is undefined or an empty string
|
||||
const filteredMods = data.filter(
|
||||
(mod) => mod.vulnerabilities && mod.vulnerabilities.trim() !== ""
|
||||
);
|
||||
setMods(filteredMods);
|
||||
return (document.title = "Vulnerabilities - SVRJS");
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Failed to fetch vulnerabilities");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
fetchMods();
|
||||
const interval = setInterval(() => {
|
||||
fetchData();
|
||||
fetchMods();
|
||||
}, 10000);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
fetchMods();
|
||||
const interval = setInterval(() => {
|
||||
fetchData();
|
||||
fetchMods();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const reversedDownloads = [...downloads].reverse();
|
||||
const reversedMods = [...mods].reverse();
|
||||
const reversedDownloads = [...downloads].reverse();
|
||||
const reversedMods = [...mods].reverse();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<head>
|
||||
<title>Vulnerabilities - SVRJS</title>
|
||||
</head>
|
||||
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||
<div className="mb-3">
|
||||
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<head>
|
||||
<title>Vulnerabilities - SVRJS</title>
|
||||
</head>
|
||||
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||
<div className="mb-3">
|
||||
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id="vulnerabilities"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
SVR.JS Vulnerabilities
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Some older versions of SVR.JS are vulnerable to cyberattacks. It's
|
||||
recommended to update your SVR.JS version to the newest one. If you find
|
||||
a security issue with SVR.JS, report it as soon as possible to
|
||||
vulnerability-reports[at]svrjs[dot]org. We'll mitigate that
|
||||
vulnerability if it is possible.
|
||||
</p>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
return (
|
||||
<section
|
||||
id="vulnerabilities"
|
||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
SVR.JS Vulnerabilities
|
||||
</h1>
|
||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||
Some older versions of SVR.JS are vulnerable to cyberattacks. It's
|
||||
recommended to update your SVR.JS version to the newest one. If you find
|
||||
a security issue with SVR.JS, report it as soon as possible to
|
||||
vulnerability-reports[at]svrjs[dot]org. We'll mitigate that
|
||||
vulnerability if it is possible.
|
||||
</p>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
|
||||
{reversedDownloads.map((download) => (
|
||||
<div
|
||||
key={download._id}
|
||||
className="flex-start flex-col prose dark:prose-invert gap-4"
|
||||
>
|
||||
<h2 className="font-semibold text-3xl -mb-2">{download.version}</h2>
|
||||
<ul className="list-disc pl-5">
|
||||
{(download.bullets ?? []).map((bullet, index) => (
|
||||
<li key={index}>{bullet.point}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
{reversedDownloads.map((download) => (
|
||||
<div
|
||||
key={download._id}
|
||||
className="flex-start flex-col prose dark:prose-invert gap-4"
|
||||
>
|
||||
<h2 className="font-semibold text-3xl -mb-2">{download.version}</h2>
|
||||
<ul className="list-disc pl-5">
|
||||
{(download.bullets ?? []).map((bullet, index) => (
|
||||
<li key={index}>{bullet.point}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert mb-6 md:mb-9">
|
||||
<ReactMarkdown>{VULNERABILITY}</ReactMarkdown>
|
||||
</div>
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert mb-6 md:mb-9">
|
||||
<ReactMarkdown>{VULNERABILITY}</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
{/* Section with MODS content */}
|
||||
{reversedMods.map((mod) => (
|
||||
<div
|
||||
key={mod._id}
|
||||
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">
|
||||
{mod.title}
|
||||
</h2>
|
||||
{mod.vulnerabilities && (
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert ">
|
||||
<ReactMarkdown>{mod.vulnerabilities}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
{/* Section with MODS content */}
|
||||
{reversedMods.map((mod) => (
|
||||
<div
|
||||
key={mod._id}
|
||||
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">
|
||||
{mod.title}
|
||||
</h2>
|
||||
{mod.vulnerabilities && (
|
||||
<div className="prose max-w-full md:prose-lg dark:prose-invert ">
|
||||
<ReactMarkdown>{mod.vulnerabilities}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Vulnerabilities;
|
||||
|
|
|
@ -9,7 +9,7 @@ export const authOptions: NextAuthOptions = {
|
|||
name: "Credentials",
|
||||
credentials: {
|
||||
username: { label: "Username", type: "text" },
|
||||
password: { label: "Password", type: "password" },
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
async authorize(credentials: any): Promise<any> {
|
||||
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.
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
|
@ -51,13 +51,13 @@ export const authOptions: NextAuthOptions = {
|
|||
// session.user.id = token.id;
|
||||
// session.user.name = token.name;
|
||||
return session;
|
||||
},
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
signIn: "/login"
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
strategy: "jwt"
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
secret: process.env.NEXTAUTH_SECRET
|
||||
};
|
||||
|
|
|
@ -2,43 +2,43 @@ import { mailOptions, transporter } from "@/lib/nodemailer/nodemailer";
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const CONTACT_MESSAGE_FIELDS: Record<string, string> = {
|
||||
name: "Name",
|
||||
email: "Email",
|
||||
message: "Message",
|
||||
name: "Name",
|
||||
email: "Email",
|
||||
message: "Message"
|
||||
};
|
||||
|
||||
const escapeHtml = (text: string) => {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
const generateEmailContent = (data: Record<string, string>) => {
|
||||
const stringData = Object.entries(data).reduce(
|
||||
(str, [key, val]) =>
|
||||
str +
|
||||
`${CONTACT_MESSAGE_FIELDS[key] || key}: ${val.replace(/\n/g, "\n")} \n\n`,
|
||||
""
|
||||
);
|
||||
const stringData = Object.entries(data).reduce(
|
||||
(str, [key, val]) =>
|
||||
str +
|
||||
`${CONTACT_MESSAGE_FIELDS[key] || key}: ${val.replace(/\n/g, "\n")} \n\n`,
|
||||
""
|
||||
);
|
||||
|
||||
const htmlData = Object.entries(data).reduce(
|
||||
(str, [key, val]) =>
|
||||
str +
|
||||
`<h3 class="form-heading">${escapeHtml(
|
||||
CONTACT_MESSAGE_FIELDS[key] || key
|
||||
)}</h3><p class="form-answer">${escapeHtml(val).replace(
|
||||
/\n/g,
|
||||
"<br/>"
|
||||
)}</p>`,
|
||||
""
|
||||
);
|
||||
const htmlData = Object.entries(data).reduce(
|
||||
(str, [key, val]) =>
|
||||
str +
|
||||
`<h3 class="form-heading">${escapeHtml(
|
||||
CONTACT_MESSAGE_FIELDS[key] || key
|
||||
)}</h3><p class="form-answer">${escapeHtml(val).replace(
|
||||
/\n/g,
|
||||
"<br/>"
|
||||
)}</p>`,
|
||||
""
|
||||
);
|
||||
|
||||
return {
|
||||
text: stringData,
|
||||
html: `<!DOCTYPE html>
|
||||
return {
|
||||
text: stringData,
|
||||
html: `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Contact Email</title>
|
||||
|
@ -90,37 +90,37 @@ const generateEmailContent = (data: Record<string, string>) => {
|
|||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`,
|
||||
};
|
||||
</html>`
|
||||
};
|
||||
};
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
if (req.method !== "POST") {
|
||||
return NextResponse.json(
|
||||
{ message: "Method Not Allowed" },
|
||||
{ status: 405 }
|
||||
);
|
||||
}
|
||||
if (req.method !== "POST") {
|
||||
return NextResponse.json(
|
||||
{ message: "Method Not Allowed" },
|
||||
{ status: 405 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await req.json();
|
||||
console.log(data);
|
||||
try {
|
||||
const data = await req.json();
|
||||
console.log(data);
|
||||
|
||||
await transporter.sendMail({
|
||||
...mailOptions,
|
||||
...generateEmailContent(data),
|
||||
subject: "Contact Email",
|
||||
});
|
||||
await transporter.sendMail({
|
||||
...mailOptions,
|
||||
...generateEmailContent(data),
|
||||
subject: "Contact Email"
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Email sent successfully" },
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Internal Server Error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: "Email sent successfully" },
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Internal Server Error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,27 +4,27 @@ import { ObjectId } from "mongodb";
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const { id } = params;
|
||||
const { id } = params;
|
||||
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const collection = db.collection("downloads");
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
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) {
|
||||
return NextResponse.json({ message: "Log deleted successfully" });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete log", error: error },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
if (result.deletedCount === 1) {
|
||||
return NextResponse.json({ message: "Log deleted successfully" });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete log", error: error },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,27 +4,27 @@ import { ObjectId } from "mongodb";
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const { id } = params;
|
||||
const { id } = params;
|
||||
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const collection = db.collection("logs");
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
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) {
|
||||
return NextResponse.json({ message: "Log deleted successfully" });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete log", error: error },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
if (result.deletedCount === 1) {
|
||||
return NextResponse.json({ message: "Log deleted successfully" });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete log", error: error },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,27 +4,27 @@ import { ObjectId } from "mongodb";
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const { id } = params;
|
||||
const { id } = params;
|
||||
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const collection = db.collection("mods");
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
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) {
|
||||
return NextResponse.json({ message: "Log deleted successfully" });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete log", error: error },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
if (result.deletedCount === 1) {
|
||||
return NextResponse.json({ message: "Log deleted successfully" });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete log", error: error },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,36 +3,36 @@ import { ObjectId } from "mongodb";
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const { id } = params;
|
||||
const { id } = params;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ message: "ID is required" }, { status: 400 });
|
||||
}
|
||||
if (!id) {
|
||||
return NextResponse.json({ message: "ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const result = await db
|
||||
.collection("vulnerabilities")
|
||||
.deleteOne({ _id: new ObjectId(id) });
|
||||
if (result.deletedCount === 1) {
|
||||
return NextResponse.json(
|
||||
{ message: "Vulnerability deleted successfully" },
|
||||
{ status: 200 }
|
||||
);
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ message: "Vulnerability not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete vulnerability" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const result = await db
|
||||
.collection("vulnerabilities")
|
||||
.deleteOne({ _id: new ObjectId(id) });
|
||||
if (result.deletedCount === 1) {
|
||||
return NextResponse.json(
|
||||
{ message: "Vulnerability deleted successfully" },
|
||||
{ status: 200 }
|
||||
);
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ message: "Vulnerability not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete vulnerability" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,30 +5,30 @@ import { serialize } from "cookie";
|
|||
export const dynamic = "force-dynamic";
|
||||
|
||||
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 adminPassword = process.env.ADMIN_PASSWORD;
|
||||
const adminUsername = process.env.ADMIN_USERNAME;
|
||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||
|
||||
if (username === adminUsername && password === adminPassword) {
|
||||
const cookie = serialize("auth", "authenticated", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24, // 1 day
|
||||
});
|
||||
if (username === adminUsername && password === adminPassword) {
|
||||
const cookie = serialize("auth", "authenticated", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 // 1 day
|
||||
});
|
||||
|
||||
return new NextResponse(JSON.stringify({ message: "Login successful" }), {
|
||||
headers: {
|
||||
"Set-Cookie": cookie,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
return new NextResponse(JSON.stringify({ message: "Login successful" }), {
|
||||
headers: {
|
||||
"Set-Cookie": cookie,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), {
|
||||
status: 401,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), {
|
||||
status: 401,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,15 +6,15 @@ export const dynamic = "force-dynamic";
|
|||
|
||||
// Handler for GET requests
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const downloads = await db.collection("logs").find().toArray();
|
||||
return NextResponse.json(downloads, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch logs" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const downloads = await db.collection("logs").find().toArray();
|
||||
return NextResponse.json(downloads, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch logs" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,104 +2,104 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
import clientPromise from "@/lib/db";
|
||||
|
||||
export const GET = async (
|
||||
req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) => {
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const { slug } = params;
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const { slug } = params;
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||
}
|
||||
if (!slug) {
|
||||
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) {
|
||||
return NextResponse.json(page, { status: 200 });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||
}
|
||||
if (page) {
|
||||
return NextResponse.json(page, { status: 200 });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PUT = async (
|
||||
req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) => {
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const { slug } = params;
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const { slug } = params;
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||
}
|
||||
if (!slug) {
|
||||
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { title, content, vulnerabilities } = await req.json();
|
||||
const { title, content, vulnerabilities } = await req.json();
|
||||
|
||||
if (
|
||||
typeof title !== "string" ||
|
||||
typeof content !== "string" ||
|
||||
typeof vulnerabilities !== "string"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid title, content, or vulnerabilities" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof title !== "string" ||
|
||||
typeof content !== "string" ||
|
||||
typeof vulnerabilities !== "string"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid title, content, or vulnerabilities" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db.collection("pages").findOneAndUpdate(
|
||||
{ slug },
|
||||
{ $set: { title, content, vulnerabilities } },
|
||||
{ returnDocument: "after" } // Updated option
|
||||
);
|
||||
try {
|
||||
const result = await db.collection("pages").findOneAndUpdate(
|
||||
{ slug },
|
||||
{ $set: { title, content, vulnerabilities } },
|
||||
{ returnDocument: "after" } // Updated option
|
||||
);
|
||||
|
||||
if (result?.value) {
|
||||
const serializedResult = {
|
||||
...result.value,
|
||||
_id: result.value._id.toString(), // Convert ObjectId to string
|
||||
};
|
||||
return NextResponse.json(serializedResult, { status: 200 });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating page:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to update page" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
if (result?.value) {
|
||||
const serializedResult = {
|
||||
...result.value,
|
||||
_id: result.value._id.toString() // Convert ObjectId to string
|
||||
};
|
||||
return NextResponse.json(serializedResult, { status: 200 });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating page:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to update page" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE = async (
|
||||
req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) => {
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const { slug } = params;
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const { slug } = params;
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||
}
|
||||
if (!slug) {
|
||||
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db.collection("pages").deleteOne({ slug });
|
||||
try {
|
||||
const result = await db.collection("pages").deleteOne({ slug });
|
||||
|
||||
if (result.deletedCount > 0) {
|
||||
return NextResponse.json(
|
||||
{ message: "Page deleted successfully" },
|
||||
{ status: 200 }
|
||||
);
|
||||
} else {
|
||||
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting page:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete page" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
if (result.deletedCount > 0) {
|
||||
return NextResponse.json(
|
||||
{ message: "Page deleted successfully" },
|
||||
{ status: 200 }
|
||||
);
|
||||
} else {
|
||||
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting page:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to delete page" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,42 +2,42 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
import clientPromise from "@/lib/db";
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
|
||||
try {
|
||||
const pages = await db.collection("pages").find().toArray();
|
||||
return NextResponse.json(pages, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error fetching pages:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to fetch pages" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
try {
|
||||
const pages = await db.collection("pages").find().toArray();
|
||||
return NextResponse.json(pages, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error fetching pages:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to fetch pages" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const { title, slug, content } = await req.json();
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
const { title, slug, content } = await req.json();
|
||||
|
||||
if (!slug) {
|
||||
return NextResponse.json(
|
||||
{ message: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!slug) {
|
||||
return NextResponse.json(
|
||||
{ message: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const newPage = { title, slug, content };
|
||||
const result = await db.collection("pages").insertOne(newPage);
|
||||
return NextResponse.json(newPage, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating page:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to create page" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
try {
|
||||
const newPage = { title, slug, content };
|
||||
const result = await db.collection("pages").insertOne(newPage);
|
||||
return NextResponse.json(newPage, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating page:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Failed to create page" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,63 +3,63 @@ import nodemailer from "nodemailer";
|
|||
import clientPromise from "@/lib/db";
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: "smtp.ethereal.email", //replace this also comment this if u not using etheral
|
||||
// service: "gmail", // uncomment if u using gmail
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.EMAIL,
|
||||
pass: process.env.EMAIL_PASS,
|
||||
},
|
||||
host: "smtp.ethereal.email", //replace this also comment this if u not using etheral
|
||||
// service: "gmail", // uncomment if u using gmail
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.EMAIL,
|
||||
pass: process.env.EMAIL_PASS
|
||||
}
|
||||
});
|
||||
|
||||
const sendEmail = async (to: string[], subject: string, html: string) => {
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: process.env.EMAIL_USER,
|
||||
to: to.join(", "),
|
||||
subject: subject,
|
||||
html: html,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
throw new Error("Failed to send email");
|
||||
}
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: process.env.EMAIL_USER,
|
||||
to: to.join(", "),
|
||||
subject: subject,
|
||||
html: html
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
throw new Error("Failed to send email");
|
||||
}
|
||||
};
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { subject, html } = await req.json();
|
||||
try {
|
||||
const { subject, html } = await req.json();
|
||||
|
||||
const client = await clientPromise;
|
||||
const db = client.db("newsletter");
|
||||
const collection = db.collection("subscribers");
|
||||
const client = await clientPromise;
|
||||
const db = client.db("newsletter");
|
||||
const collection = db.collection("subscribers");
|
||||
|
||||
const subscribers = await collection
|
||||
.find({}, { projection: { email: 1 } })
|
||||
.toArray();
|
||||
const subscribers = await collection
|
||||
.find({}, { projection: { email: 1 } })
|
||||
.toArray();
|
||||
|
||||
if (subscribers.length === 0) {
|
||||
console.error("No subscribers found in the database.");
|
||||
return NextResponse.json(
|
||||
{ message: "No subscribers found." },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
if (subscribers.length === 0) {
|
||||
console.error("No subscribers found in the database.");
|
||||
return NextResponse.json(
|
||||
{ message: "No subscribers found." },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const emails = subscribers.map((subscriber) => subscriber.email);
|
||||
const emails = subscribers.map((subscriber) => subscriber.email);
|
||||
|
||||
if (emails.length === 0) {
|
||||
console.error("No email addresses found.");
|
||||
return NextResponse.json(
|
||||
{ message: "No email addresses found." },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
if (emails.length === 0) {
|
||||
console.error("No email addresses found.");
|
||||
return NextResponse.json(
|
||||
{ message: "No email addresses found." },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
await sendEmail(emails, subject, html);
|
||||
return NextResponse.json({ message: "Emails sent successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error handling POST request:", error);
|
||||
return NextResponse.error();
|
||||
}
|
||||
await sendEmail(emails, subject, html);
|
||||
return NextResponse.json({ message: "Emails sent successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error handling POST request:", error);
|
||||
return NextResponse.error();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,43 +2,43 @@ import { NextResponse } from "next/server";
|
|||
import clientPromise from "@/lib/db";
|
||||
|
||||
interface Subscriber {
|
||||
email: string;
|
||||
subscribedAt: Date;
|
||||
email: string;
|
||||
subscribedAt: Date;
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
||||
const limit = 10;
|
||||
const skip = (page - 1) * limit;
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
||||
const limit = 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const client = await clientPromise;
|
||||
const db = client.db("newsletter");
|
||||
const collection = db.collection("subscribers");
|
||||
const client = await clientPromise;
|
||||
const db = client.db("newsletter");
|
||||
const collection = db.collection("subscribers");
|
||||
|
||||
// Pagination
|
||||
const documents = await collection.find().skip(skip).limit(limit).toArray();
|
||||
// Pagination
|
||||
const documents = await collection.find().skip(skip).limit(limit).toArray();
|
||||
|
||||
const subscribers: Subscriber[] = documents.map((doc) => ({
|
||||
email: doc.email,
|
||||
subscribedAt:
|
||||
doc.subscribedAt instanceof Date
|
||||
? doc.subscribedAt
|
||||
: new Date(doc.subscribedAt),
|
||||
}));
|
||||
const subscribers: Subscriber[] = documents.map((doc) => ({
|
||||
email: doc.email,
|
||||
subscribedAt:
|
||||
doc.subscribedAt instanceof Date
|
||||
? doc.subscribedAt
|
||||
: new Date(doc.subscribedAt)
|
||||
}));
|
||||
|
||||
const totalSubscribers = await collection.countDocuments();
|
||||
const totalSubscribers = await collection.countDocuments();
|
||||
|
||||
return NextResponse.json({
|
||||
subscribers,
|
||||
totalSubscribers,
|
||||
totalPages: Math.ceil(totalSubscribers / limit),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching subscribers:", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
return NextResponse.json({
|
||||
subscribers,
|
||||
totalSubscribers,
|
||||
totalPages: Math.ceil(totalSubscribers / limit)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching subscribers:", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,51 +2,51 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
import nodemailer from "nodemailer";
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: "smtp.ethereal.email", // Replace with your SMTP host
|
||||
// service: "gmail", // Uncomment if using Gmail
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.EMAIL,
|
||||
pass: process.env.EMAIL_PASS,
|
||||
},
|
||||
host: "smtp.ethereal.email", // Replace with your SMTP host
|
||||
// service: "gmail", // Uncomment if using Gmail
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.EMAIL,
|
||||
pass: process.env.EMAIL_PASS
|
||||
}
|
||||
});
|
||||
|
||||
const sendEmail = async (to: string[], subject: string, html: string) => {
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: process.env.EMAIL_USER,
|
||||
to: to.join(", "),
|
||||
subject: subject,
|
||||
html: html,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
throw new Error("Failed to send email");
|
||||
}
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: process.env.EMAIL_USER,
|
||||
to: to.join(", "),
|
||||
subject: subject,
|
||||
html: html
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
throw new Error("Failed to send email");
|
||||
}
|
||||
};
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { subject, html } = await req.json();
|
||||
try {
|
||||
const { subject, html } = await req.json();
|
||||
|
||||
// add ur email here
|
||||
const testEmails = [
|
||||
"abhijitbhattacharjee333@gmail.com",
|
||||
"test2@example.com",
|
||||
];
|
||||
// add ur email here
|
||||
const testEmails = [
|
||||
"abhijitbhattacharjee333@gmail.com",
|
||||
"test2@example.com"
|
||||
];
|
||||
|
||||
if (testEmails.length === 0) {
|
||||
console.error("No email addresses provided.");
|
||||
return NextResponse.json(
|
||||
{ message: "No email addresses provided." },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
if (testEmails.length === 0) {
|
||||
console.error("No email addresses provided.");
|
||||
return NextResponse.json(
|
||||
{ message: "No email addresses provided." },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
await sendEmail(testEmails, subject, html);
|
||||
return NextResponse.json({ message: "Emails sent successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error handling POST request:", error);
|
||||
return NextResponse.error();
|
||||
}
|
||||
await sendEmail(testEmails, subject, html);
|
||||
return NextResponse.json({ message: "Emails sent successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error handling POST request:", error);
|
||||
return NextResponse.error();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,62 +2,62 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
import clientPromise from "@/lib/db";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { email, captchaToken } = await req.json();
|
||||
try {
|
||||
const { email, captchaToken } = await req.json();
|
||||
|
||||
if (!email || !captchaToken) {
|
||||
return NextResponse.json(
|
||||
{ message: "Email and captcha token are required." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!email || !captchaToken) {
|
||||
return NextResponse.json(
|
||||
{ message: "Email and captcha token are required." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify hCaptcha token
|
||||
const hcaptchaResponse = await fetch(
|
||||
`https://api.hcaptcha.com/siteverify`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `secret=${process.env.HCAPTCHA_SECRET}&response=${captchaToken}`,
|
||||
}
|
||||
);
|
||||
// Verify hCaptcha token
|
||||
const hcaptchaResponse = await fetch(
|
||||
`https://api.hcaptcha.com/siteverify`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: `secret=${process.env.HCAPTCHA_SECRET}&response=${captchaToken}`
|
||||
}
|
||||
);
|
||||
|
||||
const hcaptchaData = await hcaptchaResponse.json();
|
||||
const hcaptchaData = await hcaptchaResponse.json();
|
||||
|
||||
if (!hcaptchaData.success) {
|
||||
return NextResponse.json(
|
||||
{ message: "Captcha verification failed." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!hcaptchaData.success) {
|
||||
return NextResponse.json(
|
||||
{ message: "Captcha verification failed." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const client = await clientPromise;
|
||||
const db = client.db("newsletter");
|
||||
const collection = db.collection("subscribers");
|
||||
const client = await clientPromise;
|
||||
const db = client.db("newsletter");
|
||||
const collection = db.collection("subscribers");
|
||||
|
||||
// checking if email alr exists
|
||||
const existingSubscriber = await collection.findOne({ email });
|
||||
if (existingSubscriber) {
|
||||
return NextResponse.json(
|
||||
{ message: "This email is already subscribed." },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
// checking if email alr exists
|
||||
const existingSubscriber = await collection.findOne({ email });
|
||||
if (existingSubscriber) {
|
||||
return NextResponse.json(
|
||||
{ message: "This email is already subscribed." },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// saves the email in the db
|
||||
await collection.insertOne({ email, subscribedAt: new Date() });
|
||||
// saves the email in the db
|
||||
await collection.insertOne({ email, subscribedAt: new Date() });
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Successfully subscribed!" },
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error subscribing:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Internal Server Error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: "Successfully subscribed!" },
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error subscribing:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Internal Server Error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,42 +5,42 @@ import { ObjectId } from "mongodb";
|
|||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const { id } = params;
|
||||
const body = await request.json();
|
||||
const { fileName, version, downloadLink, fileSize } = body;
|
||||
const { id } = params;
|
||||
const body = await request.json();
|
||||
const { fileName, version, downloadLink, fileSize } = body;
|
||||
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
|
||||
const result = await db.collection("mods").updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
fileName,
|
||||
version,
|
||||
downloadLink,
|
||||
fileSize,
|
||||
},
|
||||
}
|
||||
);
|
||||
const result = await db.collection("mods").updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{
|
||||
$set: {
|
||||
fileName,
|
||||
version,
|
||||
downloadLink,
|
||||
fileSize
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
return NextResponse.json({ success: true });
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "No document updated",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Update failed", error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "Failed to update mod",
|
||||
});
|
||||
}
|
||||
if (result.modifiedCount > 0) {
|
||||
return NextResponse.json({ success: true });
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "No document updated"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Update failed", error);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "Failed to update mod"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export async function POST(request: Request) {
|
|||
fileName,
|
||||
version,
|
||||
downloadLink,
|
||||
fileSize,
|
||||
fileSize
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, id: result.insertedId });
|
||||
|
|
|
@ -5,17 +5,17 @@ import clientPromise from "@/lib/db";
|
|||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const { version, date, bullets } = body;
|
||||
const body = await request.json();
|
||||
const { version, date, bullets } = body;
|
||||
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
|
||||
const result = await db.collection("logs").insertOne({
|
||||
version,
|
||||
date,
|
||||
bullets,
|
||||
});
|
||||
const result = await db.collection("logs").insertOne({
|
||||
version,
|
||||
date,
|
||||
bullets
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, id: result.insertedId });
|
||||
return NextResponse.json({ success: true, id: result.insertedId });
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export async function POST(request: Request) {
|
|||
fileName,
|
||||
version,
|
||||
downloadLink,
|
||||
fileSize,
|
||||
fileSize
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, id: result.insertedId });
|
||||
|
|
|
@ -6,11 +6,11 @@ const f = createUploadthing();
|
|||
// const auth = (req: Request) => ({ id: "fakeId" });
|
||||
|
||||
export const ourFileRouter = {
|
||||
imageUploader: f({
|
||||
"application/zip": { maxFileSize: "8MB" },
|
||||
}).onUploadComplete(async ({ metadata, file }) => {
|
||||
console.log("file url", file.url);
|
||||
}),
|
||||
imageUploader: f({
|
||||
"application/zip": { maxFileSize: "8MB" }
|
||||
}).onUploadComplete(async ({ metadata, file }) => {
|
||||
console.log("file url", file.url);
|
||||
})
|
||||
} satisfies FileRouter;
|
||||
|
||||
export type OurFileRouter = typeof ourFileRouter;
|
||||
|
|
|
@ -4,5 +4,5 @@ import { ourFileRouter } from "./core";
|
|||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const { GET, POST } = createRouteHandler({
|
||||
router: ourFileRouter,
|
||||
router: ourFileRouter
|
||||
});
|
||||
|
|
|
@ -5,16 +5,16 @@ import clientPromise from "@/lib/db";
|
|||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const { version, date, bullets } = body;
|
||||
const body = await request.json();
|
||||
const { version, date, bullets } = body;
|
||||
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
|
||||
const result = await db.collection("vulnerabilities").insertOne({
|
||||
version,
|
||||
bullets,
|
||||
});
|
||||
const result = await db.collection("vulnerabilities").insertOne({
|
||||
version,
|
||||
bullets
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, id: result.insertedId });
|
||||
return NextResponse.json({ success: true, id: result.insertedId });
|
||||
}
|
||||
|
|
|
@ -6,15 +6,15 @@ export const dynamic = "force-dynamic";
|
|||
|
||||
// Handler for GET requests
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const downloads = await db.collection("vulnerabilities").find().toArray();
|
||||
return NextResponse.json(downloads, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch logs" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("downloadsDatabase");
|
||||
const downloads = await db.collection("vulnerabilities").find().toArray();
|
||||
return NextResponse.json(downloads, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch logs" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,60 +6,59 @@ import { Toaster } from "@/components/ui/toaster";
|
|||
import { Analytics } from "@vercel/analytics/react";
|
||||
|
||||
const poppins = Poppins({
|
||||
weight: ["400", "600", "700", "900"],
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "600", "700", "900"],
|
||||
subsets: ["latin"]
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
openGraph: {
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
url: "https://svrjs.org",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "SVRJS - A Web Server running on Node.js",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS",
|
||||
},
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
openGraph: {
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
url: "https://svrjs.org",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: "SVRJS - A Web Server running on Node.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@SVR_JS",
|
||||
title: "SVRJS - A Web Server running on Node.js",
|
||||
description:
|
||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
||||
creator: "@SVR_JS"
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`antialiased ${poppins.className}`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
|
||||
{children}
|
||||
<Toaster />
|
||||
<Analytics />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`antialiased ${poppins.className}`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
<Analytics />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ const LoginPage = () => {
|
|||
const res = await signIn("credentials", {
|
||||
redirect: false,
|
||||
username,
|
||||
password,
|
||||
password
|
||||
});
|
||||
|
||||
if (res?.ok) {
|
||||
|
|
|
@ -3,28 +3,28 @@ import Navbar from "@/components/shared/Navbar";
|
|||
import Link from "next/link";
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col min-h-screen">
|
||||
<Navbar />
|
||||
<section
|
||||
id="404error"
|
||||
className="flex-center flex-col wrapper container flex-1 flex-grow"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl text-center">
|
||||
<span className="text-red-500">404</span> Page not Found
|
||||
</h1>
|
||||
<p className="text-lg mt-3 text-muted-foreground">
|
||||
Please return back to{" "}
|
||||
<Link href="/" className="underline font-bold">
|
||||
Home
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col min-h-screen">
|
||||
<Navbar />
|
||||
<section
|
||||
id="404error"
|
||||
className="flex-center flex-col wrapper container flex-1 flex-grow"
|
||||
>
|
||||
<h1 className="text-3xl md:text-5xl text-center">
|
||||
<span className="text-red-500">404</span> Page not Found
|
||||
</h1>
|
||||
<p className="text-lg mt-3 text-muted-foreground">
|
||||
Please return back to{" "}
|
||||
<Link href="/" className="underline font-bold">
|
||||
Home
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
|
|
|
@ -4,13 +4,13 @@ import { client } from "@/lib/sanity";
|
|||
import { toHTML } from "@portabletext/to-html";
|
||||
|
||||
export async function GET() {
|
||||
// Define the site URL based on the environment
|
||||
const SITE_URL =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "https://svrjs.vercel.app"
|
||||
: "http://localhost:3000";
|
||||
// Define the site URL based on the environment
|
||||
const SITE_URL =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "https://svrjs.vercel.app"
|
||||
: "http://localhost:3000";
|
||||
|
||||
const postsQuery = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||
const postsQuery = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||
title,
|
||||
"slug": slug.current,
|
||||
content,
|
||||
|
@ -18,33 +18,33 @@ export async function GET() {
|
|||
_createdAt
|
||||
}`;
|
||||
|
||||
const posts = await client.fetch(postsQuery);
|
||||
const posts = await client.fetch(postsQuery);
|
||||
|
||||
const feed = new RSS({
|
||||
title: "SVRJS Blog",
|
||||
description: "Explore the latest blog posts from SVRJS",
|
||||
feed_url: `${SITE_URL}/rss.xml`,
|
||||
site_url: `${SITE_URL}`,
|
||||
image_url: `${SITE_URL}/metadata/svrjs-cover.png`,
|
||||
language: "en-US",
|
||||
pubDate: new Date().toUTCString(),
|
||||
});
|
||||
const feed = new RSS({
|
||||
title: "SVRJS Blog",
|
||||
description: "Explore the latest blog posts from SVRJS",
|
||||
feed_url: `${SITE_URL}/rss.xml`,
|
||||
site_url: `${SITE_URL}`,
|
||||
image_url: `${SITE_URL}/metadata/svrjs-cover.png`,
|
||||
language: "en-US",
|
||||
pubDate: new Date().toUTCString()
|
||||
});
|
||||
|
||||
posts.forEach((post: any) => {
|
||||
feed.item({
|
||||
title: post.title,
|
||||
description: toHTML(post.content),
|
||||
url: `${SITE_URL}/blog/${post.slug}`,
|
||||
date: new Date(post._createdAt).toUTCString(),
|
||||
// uncomment this if u want to
|
||||
// enclosure: { url: urlFor(post.titleImage).url() },
|
||||
// author: "SVRJS",
|
||||
});
|
||||
});
|
||||
posts.forEach((post: any) => {
|
||||
feed.item({
|
||||
title: post.title,
|
||||
description: toHTML(post.content),
|
||||
url: `${SITE_URL}/blog/${post.slug}`,
|
||||
date: new Date(post._createdAt).toUTCString()
|
||||
// uncomment this if u want to
|
||||
// enclosure: { url: urlFor(post.titleImage).url() },
|
||||
// author: "SVRJS",
|
||||
});
|
||||
});
|
||||
|
||||
return new NextResponse(feed.xml({ indent: true }), {
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
});
|
||||
return new NextResponse(feed.xml({ indent: true }), {
|
||||
headers: {
|
||||
"Content-Type": "application/xml"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
import { getAllBlogPostSlugs } from "@/lib/getBlogPost";
|
||||
|
||||
export default async function sitemap() {
|
||||
const blogPostSlugs = await getAllBlogPostSlugs();
|
||||
const blogPostSlugs = await getAllBlogPostSlugs();
|
||||
|
||||
const baseRoutes = [
|
||||
"/",
|
||||
"/blog",
|
||||
"/changelogs",
|
||||
"/contact",
|
||||
"/contribute",
|
||||
"/downloads",
|
||||
"/forum",
|
||||
"/mods",
|
||||
"/privacy-policy",
|
||||
"/tos",
|
||||
"/vulnerabilities",
|
||||
"/newsletter",
|
||||
].map((route) => ({
|
||||
url: `https://svrjs.vercel.app${route}`,
|
||||
lastModified: new Date().toISOString().split("T")[0],
|
||||
}));
|
||||
const baseRoutes = [
|
||||
"/",
|
||||
"/blog",
|
||||
"/changelogs",
|
||||
"/contact",
|
||||
"/contribute",
|
||||
"/downloads",
|
||||
"/forum",
|
||||
"/mods",
|
||||
"/privacy-policy",
|
||||
"/tos",
|
||||
"/vulnerabilities",
|
||||
"/newsletter"
|
||||
].map((route) => ({
|
||||
url: `https://svrjs.vercel.app${route}`,
|
||||
lastModified: new Date().toISOString().split("T")[0]
|
||||
}));
|
||||
|
||||
const blogRoutes = blogPostSlugs.map((slug) => ({
|
||||
url: `https://svrjs.vercel.app/blog/${slug.slug}`,
|
||||
lastModified: new Date().toISOString().split("T")[0],
|
||||
}));
|
||||
const blogRoutes = blogPostSlugs.map((slug) => ({
|
||||
url: `https://svrjs.vercel.app/blog/${slug.slug}`,
|
||||
lastModified: new Date().toISOString().split("T")[0]
|
||||
}));
|
||||
|
||||
return [...baseRoutes, ...blogRoutes];
|
||||
return [...baseRoutes, ...blogRoutes];
|
||||
}
|
||||
|
|
|
@ -5,32 +5,32 @@ import { ExternalLink } from "lucide-react";
|
|||
import { client, urlFor } from "@/lib/sanity";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious
|
||||
} from "@/components/ui/pagination";
|
||||
import { format } from "date-fns";
|
||||
|
||||
interface BlogPostcard {
|
||||
title: string;
|
||||
smallDescription: string;
|
||||
currentSlug: string;
|
||||
titleImage: string;
|
||||
_createdAt: string;
|
||||
title: string;
|
||||
smallDescription: string;
|
||||
currentSlug: string;
|
||||
titleImage: string;
|
||||
_createdAt: string;
|
||||
}
|
||||
|
||||
interface BlogCardsProps {
|
||||
searchParams: { page?: string };
|
||||
searchParams: { page?: string };
|
||||
}
|
||||
|
||||
const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
|
||||
const cardsPerPage = 6;
|
||||
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
|
||||
const cardsPerPage = 6;
|
||||
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
|
||||
|
||||
const query = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||
const query = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||
title,
|
||||
smallDescription,
|
||||
"currentSlug": slug.current,
|
||||
|
@ -38,94 +38,94 @@ const BlogCards: React.FC<BlogCardsProps> = async ({ searchParams }) => {
|
|||
_createdAt
|
||||
}[${(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 totalPosts: number = await client.fetch(totalPostsQuery);
|
||||
const totalPostsQuery = `count(*[_type == 'blog'])`;
|
||||
const totalPosts: number = await client.fetch(totalPostsQuery);
|
||||
|
||||
const totalPages = Math.ceil(totalPosts / cardsPerPage);
|
||||
const totalPages = Math.ceil(totalPosts / cardsPerPage);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="grid max-w-6xl gap-4 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post, idx) => {
|
||||
const formattedDate = format(
|
||||
new Date(post._createdAt),
|
||||
"MMMM d, yyyy"
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<section className="grid max-w-6xl gap-4 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post, idx) => {
|
||||
const formattedDate = format(
|
||||
new Date(post._createdAt),
|
||||
"MMMM d, yyyy"
|
||||
);
|
||||
|
||||
const truncatedDescription =
|
||||
post.smallDescription.length > 130
|
||||
? post.smallDescription.substring(0, 130) + "..."
|
||||
: post.smallDescription;
|
||||
const truncatedDescription =
|
||||
post.smallDescription.length > 130
|
||||
? post.smallDescription.substring(0, 130) + "..."
|
||||
: post.smallDescription;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="group h-full w-full rounded-lg border overflow-hidden"
|
||||
key={idx}
|
||||
>
|
||||
<Link href={`/blog/${post.currentSlug}`} className="block">
|
||||
<div className="relative overflow-hidden rounded-t-lg">
|
||||
<Image
|
||||
src={urlFor(post.titleImage).url()}
|
||||
alt={post.title}
|
||||
width={500}
|
||||
height={300}
|
||||
priority
|
||||
className="w-full object-cover transition-transform duration-200 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex-between mb-2 py-2">
|
||||
<h3 className="text-xl font-semibold leading-tight">
|
||||
{post.title}
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground opacity-0 group-hover:opacity-100 duration-300">
|
||||
<ExternalLink />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{truncatedDescription}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Published on: {formattedDate}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<div className="flex-center mt-12">
|
||||
{totalPages > 1 && (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
{currentPage > 1 && (
|
||||
<PaginationPrevious href={`?page=${currentPage - 1}`} />
|
||||
)}
|
||||
</PaginationItem>
|
||||
{Array.from({ length: totalPages }).map((_, i) => (
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
href={`?page=${i + 1}`}
|
||||
isActive={currentPage === i + 1}
|
||||
>
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
{currentPage < totalPages && (
|
||||
<PaginationNext href={`?page=${currentPage + 1}`} />
|
||||
)}
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
className="group h-full w-full rounded-lg border overflow-hidden"
|
||||
key={idx}
|
||||
>
|
||||
<Link href={`/blog/${post.currentSlug}`} className="block">
|
||||
<div className="relative overflow-hidden rounded-t-lg">
|
||||
<Image
|
||||
src={urlFor(post.titleImage).url()}
|
||||
alt={post.title}
|
||||
width={500}
|
||||
height={300}
|
||||
priority
|
||||
className="w-full object-cover transition-transform duration-200 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex-between mb-2 py-2">
|
||||
<h3 className="text-xl font-semibold leading-tight">
|
||||
{post.title}
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground opacity-0 group-hover:opacity-100 duration-300">
|
||||
<ExternalLink />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{truncatedDescription}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Published on: {formattedDate}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<div className="flex-center mt-12">
|
||||
{totalPages > 1 && (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
{currentPage > 1 && (
|
||||
<PaginationPrevious href={`?page=${currentPage - 1}`} />
|
||||
)}
|
||||
</PaginationItem>
|
||||
{Array.from({ length: totalPages }).map((_, i) => (
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
href={`?page=${i + 1}`}
|
||||
isActive={currentPage === i + 1}
|
||||
>
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
{currentPage < totalPages && (
|
||||
<PaginationNext href={`?page=${currentPage + 1}`} />
|
||||
)}
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogCards;
|
||||
|
|
|
@ -4,27 +4,27 @@ import { Editor } from "@monaco-editor/react";
|
|||
import { EXAMPLE_A1 } from "@/constants";
|
||||
|
||||
interface CodeEditorProps {
|
||||
onChange: (value: string) => void;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const CodeEditor = ({ onChange }: CodeEditorProps) => {
|
||||
return (
|
||||
<div className="bg-white w-full max-w-full">
|
||||
<Editor
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
height="75vh"
|
||||
theme="vs-dark"
|
||||
defaultValue={EXAMPLE_A1}
|
||||
language={"html"}
|
||||
onChange={(newValue) => onChange(newValue || "")}
|
||||
className="bg-zinc-950 text-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="bg-white w-full max-w-full">
|
||||
<Editor
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false
|
||||
}
|
||||
}}
|
||||
height="75vh"
|
||||
theme="vs-dark"
|
||||
defaultValue={EXAMPLE_A1}
|
||||
language={"html"}
|
||||
onChange={(newValue) => onChange(newValue || "")}
|
||||
className="bg-zinc-950 text-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeEditor;
|
||||
|
|
|
@ -2,54 +2,54 @@ import Image from "next/image";
|
|||
import React from "react";
|
||||
|
||||
interface TestimonialCard {
|
||||
avatar: string;
|
||||
name: string;
|
||||
role?: string;
|
||||
testimonial: string;
|
||||
rating: number;
|
||||
avatar: string;
|
||||
name: string;
|
||||
role?: string;
|
||||
testimonial: string;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
const TestimonialCard = ({
|
||||
avatar,
|
||||
name,
|
||||
role,
|
||||
testimonial,
|
||||
rating,
|
||||
avatar,
|
||||
name,
|
||||
role,
|
||||
testimonial,
|
||||
rating
|
||||
}: TestimonialCard) => {
|
||||
return (
|
||||
<li className="inline-block w-full">
|
||||
<div className="bg-primary/10 dark:bg-[#1c1c1c] mx-auto mb-5 flex w-full cursor-default flex-col gap-4 rounded-2xl px-[30px] py-6 shadow-md transition-all hover:scale-[103%]">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<div>
|
||||
<Image
|
||||
src={`/testimonials/${avatar}.webp`}
|
||||
alt="avatar1"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="small-semibold dark:text-white">{name}</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{role}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="body-regular dark:text-white">{testimonial}</p>
|
||||
<div className="hue-rotate-90 text-lg">
|
||||
{/* <Image
|
||||
return (
|
||||
<li className="inline-block w-full">
|
||||
<div className="bg-primary/10 dark:bg-[#1c1c1c] mx-auto mb-5 flex w-full cursor-default flex-col gap-4 rounded-2xl px-[30px] py-6 shadow-md transition-all hover:scale-[103%]">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<div>
|
||||
<Image
|
||||
src={`/testimonials/${avatar}.webp`}
|
||||
alt="avatar1"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="small-semibold dark:text-white">{name}</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{role}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="body-regular dark:text-white">{testimonial}</p>
|
||||
<div className="hue-rotate-90 text-lg">
|
||||
{/* <Image
|
||||
src="/testimonials/stars.svg"
|
||||
alt="star"
|
||||
width={120}
|
||||
height={120}
|
||||
className="object-cover"
|
||||
/> */}
|
||||
{"⭐".repeat(rating)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
{"⭐".repeat(rating)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestimonialCard;
|
||||
|
|
|
@ -13,11 +13,11 @@ import "prismjs/components/prism-markup-templating";
|
|||
import "prismjs/components/prism-handlebars";
|
||||
|
||||
export default function PrismLoader() {
|
||||
useEffect(() => {
|
||||
if (Prism) {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (Prism) {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -3,40 +3,40 @@ import React from "react";
|
|||
import Statistics from "./Statistics";
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<section id="about" className="container py-2 sm:py-9">
|
||||
<div className="bg-accent/50 border rounded-lg py-12">
|
||||
<div className="px-6 flex flex-col-reverse md:flex-row gap-8 md:gap-12">
|
||||
<Image
|
||||
src="/about.svg"
|
||||
alt="aboutpicture"
|
||||
width={300}
|
||||
height={300}
|
||||
className="w-[300px] object-contain rounded-lg flex-shrink-0"
|
||||
/>
|
||||
return (
|
||||
<section id="about" className="container py-2 sm:py-9">
|
||||
<div className="bg-accent/50 border rounded-lg py-12">
|
||||
<div className="px-6 flex flex-col-reverse md:flex-row gap-8 md:gap-12">
|
||||
<Image
|
||||
src="/about.svg"
|
||||
alt="aboutpicture"
|
||||
width={300}
|
||||
height={300}
|
||||
className="w-[300px] object-contain rounded-lg flex-shrink-0"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col justify-between">
|
||||
<div className="pb-6">
|
||||
<h2 className="text-3xl md:text-5xl font-bold">
|
||||
About{" "}
|
||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||
SVRJS!
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mt-4">
|
||||
Host a webpage, run server-side JavaScript, use mods to expand
|
||||
server functionality, or use it as a forward or reverse proxy.
|
||||
SVRJS is a web server that runs on top of Node.JS, enabling
|
||||
server-side JS on webpages. SVRJS also has an integrated log
|
||||
viewer, log highlighter, and user management tool.
|
||||
</p>
|
||||
</div>
|
||||
<Statistics />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
<div className="flex flex-col justify-between">
|
||||
<div className="pb-6">
|
||||
<h2 className="text-3xl md:text-5xl font-bold">
|
||||
About{" "}
|
||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||
SVRJS!
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mt-4">
|
||||
Host a webpage, run server-side JavaScript, use mods to expand
|
||||
server functionality, or use it as a forward or reverse proxy.
|
||||
SVRJS is a web server that runs on top of Node.JS, enabling
|
||||
server-side JS on webpages. SVRJS also has an integrated log
|
||||
viewer, log highlighter, and user management tool.
|
||||
</p>
|
||||
</div>
|
||||
<Statistics />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionTrigger
|
||||
} from "../ui/accordion";
|
||||
|
||||
const Faq = () => {
|
||||
|
|
|
@ -6,102 +6,102 @@ import { FOOTERLINKS } from "@/constants";
|
|||
import Logo from "./Logo";
|
||||
|
||||
const Footer = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
return (
|
||||
<>
|
||||
<footer className="flex flex-col w-full transition-all bg-zinc-50 text-black dark:bg-[#0308033b] border-t dark:text-white">
|
||||
<div className="px-6 md:px-12 lg:px-24 py-10 w-full mx-auto max-w-screen-2xl">
|
||||
<div className="flex flex-col lg:flex-row justify-between max-lg:items-start max-md:items-center items-center mb-6 ">
|
||||
<div className="flex items-center mb-6 lg:mb-0">
|
||||
<Logo width={200} height={80} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center md:items-start md:flex-row justify-between w-full lg:w-auto space-y-6 md:space-y-0 md:space-x-6 xl:space-x-16">
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div className="text-2xl font-light text-primary">
|
||||
Quick Links
|
||||
</div>
|
||||
{FOOTERLINKS.otherPages.map((link) => (
|
||||
<span key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
const currentYear = new Date().getFullYear();
|
||||
return (
|
||||
<>
|
||||
<footer className="flex flex-col w-full transition-all bg-zinc-50 text-black dark:bg-[#0308033b] border-t dark:text-white">
|
||||
<div className="px-6 md:px-12 lg:px-24 py-10 w-full mx-auto max-w-screen-2xl">
|
||||
<div className="flex flex-col lg:flex-row justify-between max-lg:items-start max-md:items-center items-center mb-6 ">
|
||||
<div className="flex items-center mb-6 lg:mb-0">
|
||||
<Logo width={200} height={80} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center md:items-start md:flex-row justify-between w-full lg:w-auto space-y-6 md:space-y-0 md:space-x-6 xl:space-x-16">
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div className="text-2xl font-light text-primary">
|
||||
Quick Links
|
||||
</div>
|
||||
{FOOTERLINKS.otherPages.map((link) => (
|
||||
<span key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<h1 className="text-2xl font-light text-primary">Resources</h1>
|
||||
{FOOTERLINKS.plans.map((link) => (
|
||||
<span key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div className="text-2xl font-light text-primary">
|
||||
Additional
|
||||
</div>
|
||||
{FOOTERLINKS.additional.map((link) => (
|
||||
<span key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div className="text-2xl font-light text-primary">Social</div>
|
||||
<p className="text-base font-light">
|
||||
{FOOTERLINKS.social.supportText}
|
||||
</p>
|
||||
<div className="flex space-x-1 py-3">
|
||||
<Iconss />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t mb-6 border-gray-300 dark:border-white/30"></div>
|
||||
<div className="flex flex-col lg:flex-row justify-between items-center space-y-4 lg:space-y-0 px-4">
|
||||
<span className="text-sm font-light">
|
||||
Copyright © 2023-{currentYear}{" "}
|
||||
<Link
|
||||
href={FOOTERLINKS.footerBottom.rightsReserved.href}
|
||||
className="text-primary font-semibold"
|
||||
>
|
||||
{FOOTERLINKS.footerBottom.rightsReserved.label}
|
||||
</Link>
|
||||
</span>
|
||||
<span className="text-sm font-light">
|
||||
<Link
|
||||
href={FOOTERLINKS.footerBottom.termsofService.href}
|
||||
className="text-primary font-medium transition-all underline-offset-4 hover:underline"
|
||||
>
|
||||
{FOOTERLINKS.footerBottom.termsofService.label}{" "}
|
||||
</Link>
|
||||
|{" "}
|
||||
<Link
|
||||
href={FOOTERLINKS.footerBottom.privacyPolicy.href}
|
||||
className="text-primary font-medium transition-all underline-offset-4 hover:underline"
|
||||
>
|
||||
{FOOTERLINKS.footerBottom.privacyPolicy.label}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<h1 className="text-2xl font-light text-primary">Resources</h1>
|
||||
{FOOTERLINKS.plans.map((link) => (
|
||||
<span key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div className="text-2xl font-light text-primary">
|
||||
Additional
|
||||
</div>
|
||||
{FOOTERLINKS.additional.map((link) => (
|
||||
<span key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-base font-light dark:hover:text-green-100/70 hover:text-green-500 hover:underline"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div className="text-2xl font-light text-primary">Social</div>
|
||||
<p className="text-base font-light">
|
||||
{FOOTERLINKS.social.supportText}
|
||||
</p>
|
||||
<div className="flex space-x-1 py-3">
|
||||
<Iconss />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t mb-6 border-gray-300 dark:border-white/30"></div>
|
||||
<div className="flex flex-col lg:flex-row justify-between items-center space-y-4 lg:space-y-0 px-4">
|
||||
<span className="text-sm font-light">
|
||||
Copyright © 2023-{currentYear}{" "}
|
||||
<Link
|
||||
href={FOOTERLINKS.footerBottom.rightsReserved.href}
|
||||
className="text-primary font-semibold"
|
||||
>
|
||||
{FOOTERLINKS.footerBottom.rightsReserved.label}
|
||||
</Link>
|
||||
</span>
|
||||
<span className="text-sm font-light">
|
||||
<Link
|
||||
href={FOOTERLINKS.footerBottom.termsofService.href}
|
||||
className="text-primary font-medium transition-all underline-offset-4 hover:underline"
|
||||
>
|
||||
{FOOTERLINKS.footerBottom.termsofService.label}{" "}
|
||||
</Link>
|
||||
|{" "}
|
||||
<Link
|
||||
href={FOOTERLINKS.footerBottom.privacyPolicy.href}
|
||||
className="text-primary font-medium transition-all underline-offset-4 hover:underline"
|
||||
>
|
||||
{FOOTERLINKS.footerBottom.privacyPolicy.label}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
|
@ -11,160 +11,160 @@ import { cn } from "@/lib/utils";
|
|||
import Image from "next/image";
|
||||
|
||||
const happyMonkey = Happy_Monkey({
|
||||
preload: true,
|
||||
weight: ["400"],
|
||||
subsets: ["latin"],
|
||||
preload: true,
|
||||
weight: ["400"],
|
||||
subsets: ["latin"]
|
||||
});
|
||||
|
||||
const Hero = () => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [command, setCommand] = useState(
|
||||
"curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20240509.sh > /tmp/installer.sh && sudo bash /tmp/installer.sh"
|
||||
);
|
||||
const [selectedButton, setSelectedButton] = useState<"linux" | "docker">(
|
||||
"linux"
|
||||
);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [command, setCommand] = useState(
|
||||
"curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20240509.sh > /tmp/installer.sh && sudo bash /tmp/installer.sh"
|
||||
);
|
||||
const [selectedButton, setSelectedButton] = useState<"linux" | "docker">(
|
||||
"linux"
|
||||
);
|
||||
|
||||
const commands = {
|
||||
linux:
|
||||
"curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20240509.sh > /tmp/installer.sh && sudo bash /tmp/installer.sh",
|
||||
docker:
|
||||
"docker pull svrjs/svrjs && docker run --name mysvrjs -d -p 80:80 --restart=always svrjs/svrjs",
|
||||
};
|
||||
const commands = {
|
||||
linux:
|
||||
"curl -fsSL https://downloads.svrjs.org/installer/svr.js.installer.linux.20240509.sh > /tmp/installer.sh && sudo bash /tmp/installer.sh",
|
||||
docker:
|
||||
"docker pull svrjs/svrjs && docker run --name mysvrjs -d -p 80:80 --restart=always svrjs/svrjs"
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(command);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
};
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(command);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
};
|
||||
|
||||
const handleButtonClick = (type: "linux" | "docker") => {
|
||||
setCommand(commands[type]);
|
||||
setSelectedButton(type);
|
||||
};
|
||||
const handleButtonClick = (type: "linux" | "docker") => {
|
||||
setCommand(commands[type]);
|
||||
setSelectedButton(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative sm:container grid lg:grid-cols-2 place-items-center py-20 md:py-24 gap-10">
|
||||
<GridPattern
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]",
|
||||
"inset-x-0 inset-y-[-50%] h-[200%] opacity-30"
|
||||
)}
|
||||
/>
|
||||
<div className="text-center lg:text-start space-y-6">
|
||||
<AnimatedGradientText className="mx-auto lg:mx-0">
|
||||
🎉{" "}
|
||||
<hr className="mx-2 h-4 w-[1px] shrink-0 bg-black dark:bg-gray-300" />
|
||||
<span
|
||||
className={cn(
|
||||
`inline animate-gradient bg-gradient-to-r from-[#235b1a] to-[#315620] dark:bg-gradient-to-r dark:from-[#6df458] dark:to-[#4c932a] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`
|
||||
)}
|
||||
>
|
||||
Expanding server functionality
|
||||
</span>
|
||||
</AnimatedGradientText>
|
||||
<main className="text-5xl md:text-6xl font-bold">
|
||||
<h1 className="inline custom-title">
|
||||
<span className="text-transparent bg-gradient-to-r from-green-300 to-primary bg-clip-text">
|
||||
Simplify
|
||||
</span>{" "}
|
||||
your server logic performance
|
||||
</h1>
|
||||
</main>
|
||||
<p className="text-lg text-muted-foreground md:w-10/12 mx-auto lg:mx-0">
|
||||
SVR.JS is a web server that runs on top of Node.JS, thus enabling
|
||||
server-side JavaScript on webpages. SVR.JS also has an integrated log
|
||||
viewer and more...
|
||||
</p>
|
||||
<div className="relative mx-auto lg:mx-0 flex gap-2 flex-col-reverse lg:flex-row justify-start items-center w-fit">
|
||||
<Button
|
||||
className="w-fit"
|
||||
onClick={copyToClipboard}
|
||||
variant={!isCopied ? "secondary" : "secondary"}
|
||||
>
|
||||
{!isCopied ? (
|
||||
<Clipboard className="w-4 h-4 mr-2" />
|
||||
) : (
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{command.slice(0, 39)}...
|
||||
</Button>
|
||||
<p className="hidden lg:block">|</p>
|
||||
<p className="block lg:hidden">or</p>
|
||||
<Link className="w-full" href="/downloads">
|
||||
<Button className="w-full">Download</Button>
|
||||
</Link>
|
||||
<div className="pointer-events-none dark:invert -scale-x-100 absolute -bottom-14 max-lg:left-0 lg:right-20 inline-flex justify-center items-center gap-1">
|
||||
<Image
|
||||
src="/curly-arrow.png"
|
||||
width={35}
|
||||
height={35}
|
||||
alt="Curly arrow"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
`mt-10 font-bold text-black -scale-x-100 text-sm ${happyMonkey.className}`
|
||||
)}
|
||||
>
|
||||
Try Now!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center lg:justify-start justify-center gap-3 w-full">
|
||||
<Button
|
||||
className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${
|
||||
selectedButton === "linux"
|
||||
? "bg-accent"
|
||||
: "bg-primary-foreground/20"
|
||||
}`}
|
||||
variant={"ghost"}
|
||||
onClick={() => handleButtonClick("linux")}
|
||||
>
|
||||
<span className="sr-only">Linux</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={200}
|
||||
height={200}
|
||||
viewBox="0 0 448 512"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${
|
||||
selectedButton === "docker"
|
||||
? "bg-accent"
|
||||
: "bg-primary-foreground/20"
|
||||
}`}
|
||||
variant={"ghost"}
|
||||
onClick={() => handleButtonClick("docker")}
|
||||
>
|
||||
<span className="sr-only">Docker</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={200}
|
||||
height={200}
|
||||
viewBox="0 0 50 50"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M27.336 23.076h-5.164v-5.8h5.164v5.8Zm0-19.951h-5.164v5.928h5.164V3.125Zm6.11 14.14H28.28v5.801h5.164v-5.8Zm-12.212-7.04H16.07v5.869h5.164v-5.87Zm6.102 0h-5.164v5.869h5.164v-5.87ZM48.96 19.99c-1.125-.947-3.719-1.289-5.711-.82-.258-2.344-1.305-4.385-3.211-6.22l-1.094-.909-.726 1.367c-1.438 2.715-1.828 7.188-.29 10.137-.679.459-2.015 1.084-3.78 1.045H.186c-.68 4.96.454 11.406 3.438 15.83 2.898 4.287 7.242 6.465 12.922 6.465 12.297 0 21.398-7.08 25.656-19.942 1.672.04 5.281.01 7.133-4.414.117-.244.516-1.289.664-1.67l-1.04-.869ZM9.03 17.266H3.875v5.8h5.164v-5.8h-.008Zm6.102 0H9.969v5.8h5.164v-5.8Zm6.101 0H16.07v5.8h5.164v-5.8Zm-6.101-7.041H9.969v5.869h5.164v-5.87Z"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10">
|
||||
<HeroCards />
|
||||
</div>
|
||||
<div className="shadow"></div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section className="relative sm:container grid lg:grid-cols-2 place-items-center py-20 md:py-24 gap-10">
|
||||
<GridPattern
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]",
|
||||
"inset-x-0 inset-y-[-50%] h-[200%] opacity-30"
|
||||
)}
|
||||
/>
|
||||
<div className="text-center lg:text-start space-y-6">
|
||||
<AnimatedGradientText className="mx-auto lg:mx-0">
|
||||
🎉{" "}
|
||||
<hr className="mx-2 h-4 w-[1px] shrink-0 bg-black dark:bg-gray-300" />
|
||||
<span
|
||||
className={cn(
|
||||
`inline animate-gradient bg-gradient-to-r from-[#235b1a] to-[#315620] dark:bg-gradient-to-r dark:from-[#6df458] dark:to-[#4c932a] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`
|
||||
)}
|
||||
>
|
||||
Expanding server functionality
|
||||
</span>
|
||||
</AnimatedGradientText>
|
||||
<main className="text-5xl md:text-6xl font-bold">
|
||||
<h1 className="inline custom-title">
|
||||
<span className="text-transparent bg-gradient-to-r from-green-300 to-primary bg-clip-text">
|
||||
Simplify
|
||||
</span>{" "}
|
||||
your server logic performance
|
||||
</h1>
|
||||
</main>
|
||||
<p className="text-lg text-muted-foreground md:w-10/12 mx-auto lg:mx-0">
|
||||
SVR.JS is a web server that runs on top of Node.JS, thus enabling
|
||||
server-side JavaScript on webpages. SVR.JS also has an integrated log
|
||||
viewer and more...
|
||||
</p>
|
||||
<div className="relative mx-auto lg:mx-0 flex gap-2 flex-col-reverse lg:flex-row justify-start items-center w-fit">
|
||||
<Button
|
||||
className="w-fit"
|
||||
onClick={copyToClipboard}
|
||||
variant={!isCopied ? "secondary" : "secondary"}
|
||||
>
|
||||
{!isCopied ? (
|
||||
<Clipboard className="w-4 h-4 mr-2" />
|
||||
) : (
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{command.slice(0, 39)}...
|
||||
</Button>
|
||||
<p className="hidden lg:block">|</p>
|
||||
<p className="block lg:hidden">or</p>
|
||||
<Link className="w-full" href="/downloads">
|
||||
<Button className="w-full">Download</Button>
|
||||
</Link>
|
||||
<div className="pointer-events-none dark:invert -scale-x-100 absolute -bottom-14 max-lg:left-0 lg:right-20 inline-flex justify-center items-center gap-1">
|
||||
<Image
|
||||
src="/curly-arrow.png"
|
||||
width={35}
|
||||
height={35}
|
||||
alt="Curly arrow"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
`mt-10 font-bold text-black -scale-x-100 text-sm ${happyMonkey.className}`
|
||||
)}
|
||||
>
|
||||
Try Now!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center lg:justify-start justify-center gap-3 w-full">
|
||||
<Button
|
||||
className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${
|
||||
selectedButton === "linux"
|
||||
? "bg-accent"
|
||||
: "bg-primary-foreground/20"
|
||||
}`}
|
||||
variant={"ghost"}
|
||||
onClick={() => handleButtonClick("linux")}
|
||||
>
|
||||
<span className="sr-only">Linux</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={200}
|
||||
height={200}
|
||||
viewBox="0 0 448 512"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
className={`rounded-full w-12 h-12 lg:w-16 lg:h-16 ${
|
||||
selectedButton === "docker"
|
||||
? "bg-accent"
|
||||
: "bg-primary-foreground/20"
|
||||
}`}
|
||||
variant={"ghost"}
|
||||
onClick={() => handleButtonClick("docker")}
|
||||
>
|
||||
<span className="sr-only">Docker</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={200}
|
||||
height={200}
|
||||
viewBox="0 0 50 50"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M27.336 23.076h-5.164v-5.8h5.164v5.8Zm0-19.951h-5.164v5.928h5.164V3.125Zm6.11 14.14H28.28v5.801h5.164v-5.8Zm-12.212-7.04H16.07v5.869h5.164v-5.87Zm6.102 0h-5.164v5.869h5.164v-5.87ZM48.96 19.99c-1.125-.947-3.719-1.289-5.711-.82-.258-2.344-1.305-4.385-3.211-6.22l-1.094-.909-.726 1.367c-1.438 2.715-1.828 7.188-.29 10.137-.679.459-2.015 1.084-3.78 1.045H.186c-.68 4.96.454 11.406 3.438 15.83 2.898 4.287 7.242 6.465 12.922 6.465 12.297 0 21.398-7.08 25.656-19.942 1.672.04 5.281.01 7.133-4.414.117-.244.516-1.289.664-1.67l-1.04-.869ZM9.03 17.266H3.875v5.8h5.164v-5.8h-.008Zm6.102 0H9.969v5.8h5.164v-5.8Zm6.101 0H16.07v5.8h5.164v-5.8Zm-6.101-7.041H9.969v5.869h5.164v-5.87Z"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10">
|
||||
<HeroCards />
|
||||
</div>
|
||||
<div className="shadow"></div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
|
|
|
@ -5,12 +5,12 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import Image from "next/image";
|
||||
import { Happy_Monkey } from "next/font/google";
|
||||
|
@ -19,170 +19,170 @@ import Iconss from "../ui/icons";
|
|||
import { useRouter } from "next/navigation";
|
||||
|
||||
const happyMonkey = Happy_Monkey({
|
||||
preload: true,
|
||||
weight: ["400"],
|
||||
subsets: ["latin"],
|
||||
preload: true,
|
||||
weight: ["400"],
|
||||
subsets: ["latin"]
|
||||
});
|
||||
|
||||
const HeroCards = () => {
|
||||
const router = useRouter();
|
||||
const cards = {
|
||||
aboutCard: {
|
||||
description:
|
||||
"Discover more about our services and offerings. We aim to provide the best experience for our users.",
|
||||
socialLinks: {
|
||||
x: "https://x.com/SVR_JS",
|
||||
Mastodon: "https://mastodon.social/@svrjs",
|
||||
Bluesky: "https://bsky.app/profile/svrjs.org",
|
||||
Odysee: "https://odysee.com/@SVRJS",
|
||||
},
|
||||
},
|
||||
pricingCard: {
|
||||
planName: "Pro Plan",
|
||||
badgeTitle: "Popular",
|
||||
pricePerMonth: "$0",
|
||||
description:
|
||||
"Get the best features and priority support with our Pro Plan.",
|
||||
primaryButtonText: "Download SVR Now",
|
||||
onPrimaryButtonClick: () => router.push("/downloads"),
|
||||
features: [
|
||||
{
|
||||
title: "Unlimited Projects",
|
||||
icons: <Infinity className="rounded-full" width={25} height={25} />,
|
||||
},
|
||||
{
|
||||
title: "Priority Support",
|
||||
icons: (
|
||||
<ArchiveRestore className="rounded-full" width={25} height={25} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Free Updates",
|
||||
icons: <Headset className="rounded-full" width={25} height={25} />,
|
||||
},
|
||||
],
|
||||
curlyText: "Best Value!",
|
||||
},
|
||||
serviceCard: {
|
||||
title: "Our Services",
|
||||
description:
|
||||
"We offer a variety of services to cater to your needs, including web development, SEO, and more.",
|
||||
},
|
||||
};
|
||||
const router = useRouter();
|
||||
const cards = {
|
||||
aboutCard: {
|
||||
description:
|
||||
"Discover more about our services and offerings. We aim to provide the best experience for our users.",
|
||||
socialLinks: {
|
||||
x: "https://x.com/SVR_JS",
|
||||
Mastodon: "https://mastodon.social/@svrjs",
|
||||
Bluesky: "https://bsky.app/profile/svrjs.org",
|
||||
Odysee: "https://odysee.com/@SVRJS"
|
||||
}
|
||||
},
|
||||
pricingCard: {
|
||||
planName: "Pro Plan",
|
||||
badgeTitle: "Popular",
|
||||
pricePerMonth: "$0",
|
||||
description:
|
||||
"Get the best features and priority support with our Pro Plan.",
|
||||
primaryButtonText: "Download SVR Now",
|
||||
onPrimaryButtonClick: () => router.push("/downloads"),
|
||||
features: [
|
||||
{
|
||||
title: "Unlimited Projects",
|
||||
icons: <Infinity className="rounded-full" width={25} height={25} />
|
||||
},
|
||||
{
|
||||
title: "Priority Support",
|
||||
icons: (
|
||||
<ArchiveRestore className="rounded-full" width={25} height={25} />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Free Updates",
|
||||
icons: <Headset className="rounded-full" width={25} height={25} />
|
||||
}
|
||||
],
|
||||
curlyText: "Best Value!"
|
||||
},
|
||||
serviceCard: {
|
||||
title: "Our Services",
|
||||
description:
|
||||
"We offer a variety of services to cater to your needs, including web development, SEO, and more."
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hidden lg:flex flex-row flex-wrap gap-8 relative w-[700px] h-[500px]">
|
||||
{/* Twitter First Top left */}
|
||||
<Card className="absolute w-[340px] -top-[15px] drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||
<CardHeader className="flex flex-row items-center gap-4 pb-2">
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
alt={"twitteravatar"}
|
||||
src={"https://github.com/shadcn.png"}
|
||||
/>
|
||||
<AvatarFallback>Proxy</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<CardTitle className="text-lg">Proxy</CardTitle>
|
||||
<CardDescription>@proxyxd_s</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
Svrjs has the best server side rendering{" "}
|
||||
<span className="text-sky-400">#SVRJSONTOP</span>
|
||||
</CardContent>
|
||||
<BorderBeam className="-z-10" />
|
||||
</Card>
|
||||
return (
|
||||
<div className="hidden lg:flex flex-row flex-wrap gap-8 relative w-[700px] h-[500px]">
|
||||
{/* Twitter First Top left */}
|
||||
<Card className="absolute w-[340px] -top-[15px] drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||
<CardHeader className="flex flex-row items-center gap-4 pb-2">
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
alt={"twitteravatar"}
|
||||
src={"https://github.com/shadcn.png"}
|
||||
/>
|
||||
<AvatarFallback>Proxy</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<CardTitle className="text-lg">Proxy</CardTitle>
|
||||
<CardDescription>@proxyxd_s</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
Svrjs has the best server side rendering{" "}
|
||||
<span className="text-sky-400">#SVRJSONTOP</span>
|
||||
</CardContent>
|
||||
<BorderBeam className="-z-10" />
|
||||
</Card>
|
||||
|
||||
{/* Socials Second top right */}
|
||||
<Card className="absolute right-[20px] top-4 w-80 flex flex-col justify-center items-center drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||
<CardHeader className="flex justify-center items-center pb-2">
|
||||
<CardTitle className="text-center">Socials</CardTitle>
|
||||
<CardDescription className="font-medium text-primary"></CardDescription>
|
||||
</CardHeader>
|
||||
{/* Socials Second top right */}
|
||||
<Card className="absolute right-[20px] top-4 w-80 flex flex-col justify-center items-center drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||
<CardHeader className="flex justify-center items-center pb-2">
|
||||
<CardTitle className="text-center">Socials</CardTitle>
|
||||
<CardDescription className="font-medium text-primary"></CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="text-center text-muted-foreground pb-2">
|
||||
<p>{cards.aboutCard.description}</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Iconss />
|
||||
</CardFooter>
|
||||
<BorderBeam className="-z-10" />
|
||||
</Card>
|
||||
<CardContent className="text-center text-muted-foreground pb-2">
|
||||
<p>{cards.aboutCard.description}</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Iconss />
|
||||
</CardFooter>
|
||||
<BorderBeam className="-z-10" />
|
||||
</Card>
|
||||
|
||||
{/* Pricings Bottom left */}
|
||||
<Card className="absolute top-[170px] left-[50px] w-72 drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
{cards.pricingCard.planName}
|
||||
<Badge variant="secondary" className="text-sm text-primary">
|
||||
{cards.pricingCard.badgeTitle}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<div>
|
||||
<span className="text-3xl font-bold">
|
||||
{cards.pricingCard.pricePerMonth}
|
||||
</span>
|
||||
<span className="text-muted-foreground"> /month</span>
|
||||
</div>
|
||||
{/* Pricings Bottom left */}
|
||||
<Card className="absolute top-[170px] left-[50px] w-72 drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
{cards.pricingCard.planName}
|
||||
<Badge variant="secondary" className="text-sm text-primary">
|
||||
{cards.pricingCard.badgeTitle}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<div>
|
||||
<span className="text-3xl font-bold">
|
||||
{cards.pricingCard.pricePerMonth}
|
||||
</span>
|
||||
<span className="text-muted-foreground"> /month</span>
|
||||
</div>
|
||||
|
||||
<CardDescription>{cards.pricingCard.description}</CardDescription>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={cards.pricingCard.onPrimaryButtonClick}
|
||||
>
|
||||
{cards.pricingCard.primaryButtonText}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<hr className="w-4/5 m-auto -mt-2 mb-4" />
|
||||
<CardFooter className="flex">
|
||||
<div className="space-y-3">
|
||||
{cards.pricingCard.features.map((benefit) => (
|
||||
<span
|
||||
key={benefit.title}
|
||||
className="inline-flex justify-center items-center gap-x-3"
|
||||
>
|
||||
{benefit.icons}
|
||||
<h3>{benefit.title}</h3>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CardFooter>
|
||||
<BorderBeam className="-z-10" />
|
||||
<div className="pointer-events-none dark:invert -scale-x-100 absolute w-36 top-[9.5rem] -left-[7.5rem] inline-flex justify-center items-center gap-1">
|
||||
<Image
|
||||
src="/curly-arrow.png"
|
||||
width={35}
|
||||
height={35}
|
||||
alt="Curly arrow"
|
||||
/>
|
||||
<span
|
||||
style={happyMonkey.style}
|
||||
className="mt-10 font-bold text-black -scale-x-100 text-sm"
|
||||
>
|
||||
{cards.pricingCard.curlyText}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
<CardDescription>{cards.pricingCard.description}</CardDescription>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={cards.pricingCard.onPrimaryButtonClick}
|
||||
>
|
||||
{cards.pricingCard.primaryButtonText}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<hr className="w-4/5 m-auto -mt-2 mb-4" />
|
||||
<CardFooter className="flex">
|
||||
<div className="space-y-3">
|
||||
{cards.pricingCard.features.map((benefit) => (
|
||||
<span
|
||||
key={benefit.title}
|
||||
className="inline-flex justify-center items-center gap-x-3"
|
||||
>
|
||||
{benefit.icons}
|
||||
<h3>{benefit.title}</h3>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CardFooter>
|
||||
<BorderBeam className="-z-10" />
|
||||
<div className="pointer-events-none dark:invert -scale-x-100 absolute w-36 top-[9.5rem] -left-[7.5rem] inline-flex justify-center items-center gap-1">
|
||||
<Image
|
||||
src="/curly-arrow.png"
|
||||
width={35}
|
||||
height={35}
|
||||
alt="Curly arrow"
|
||||
/>
|
||||
<span
|
||||
style={happyMonkey.style}
|
||||
className="mt-10 font-bold text-black -scale-x-100 text-sm"
|
||||
>
|
||||
{cards.pricingCard.curlyText}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Service */}
|
||||
<Card className="absolute w-[350px] -right-[10px] bottom-[75px] drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||
<CardHeader className="space-y-1 flex md:flex-row justify-start items-start gap-4">
|
||||
<div className="mt-1 bg-primary/20 p-1 rounded-2xl">
|
||||
<LightbulbIcon className="fill-black dark:fill-[#F596D3]" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{cards.serviceCard.title}</CardTitle>
|
||||
<CardDescription className="text-md mt-2">
|
||||
{cards.serviceCard.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<BorderBeam className="-z-10" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
{/* Service */}
|
||||
<Card className="absolute w-[350px] -right-[10px] bottom-[75px] drop-shadow-xl shadow-black/10 dark:shadow-white/10">
|
||||
<CardHeader className="space-y-1 flex md:flex-row justify-start items-start gap-4">
|
||||
<div className="mt-1 bg-primary/20 p-1 rounded-2xl">
|
||||
<LightbulbIcon className="fill-black dark:fill-[#F596D3]" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{cards.serviceCard.title}</CardTitle>
|
||||
<CardDescription className="text-md mt-2">
|
||||
{cards.serviceCard.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<BorderBeam className="-z-10" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroCards;
|
||||
|
|
|
@ -3,33 +3,33 @@ import React from "react";
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||
|
||||
const HowItWorks = () => {
|
||||
return (
|
||||
<section className="container text-center py-12 sm:py-24">
|
||||
<h2 className="text-3xl md:text-5xl font-bold">
|
||||
Accelerate your{" "}
|
||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||
development
|
||||
</span>
|
||||
</h2>
|
||||
<p className="md:w-3/4 mx-auto mt-4 mb-8 text-lg md:text-xl text-muted-foreground">
|
||||
Beautifully designed components that you can copy and paste into your
|
||||
apps. Accessible. Customizable. Open Source.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{Features.map(({ icon, title, description }) => (
|
||||
<Card key={title} className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="grid gap-4 place-items-center">
|
||||
{icon}
|
||||
{title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{description}</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section className="container text-center py-12 sm:py-24">
|
||||
<h2 className="text-3xl md:text-5xl font-bold">
|
||||
Accelerate your{" "}
|
||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||
development
|
||||
</span>
|
||||
</h2>
|
||||
<p className="md:w-3/4 mx-auto mt-4 mb-8 text-lg md:text-xl text-muted-foreground">
|
||||
Beautifully designed components that you can copy and paste into your
|
||||
apps. Accessible. Customizable. Open Source.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{Features.map(({ icon, title, description }) => (
|
||||
<Card key={title} className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="grid gap-4 place-items-center">
|
||||
{icon}
|
||||
{title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>{description}</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HowItWorks;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger
|
||||
} from "../ui/sheet";
|
||||
import { Menu } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
@ -14,63 +14,63 @@ import { buttonVariants } from "../ui/button";
|
|||
import Logo from "./Logo";
|
||||
|
||||
const MobileNav = () => {
|
||||
return (
|
||||
<div className="flex md:hidden">
|
||||
<ThemeToggle />
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<span className="sr-only">Menu</span>
|
||||
<Menu className="w-5 h-5" />
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
<Logo width={120} height={40} />
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav
|
||||
id="mobile-menu"
|
||||
className="flex flex-col justify-center items-center gap-2 mt-4"
|
||||
>
|
||||
{NAVBAR.centerLinks?.map(({ href = "", label, target }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={target}
|
||||
className={buttonVariants({ variant: "ghost" })}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
{NAVBAR.rightLinks?.map(({ href, label, target }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={target}
|
||||
className={`w-[110px] gap-2 ${buttonVariants({
|
||||
variant: "secondary",
|
||||
})}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 123 123"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M120.208 55.953 66.715 2.463a7.885 7.885 0 0 0-11.158 0l-11.109 11.11 14.088 14.088a9.373 9.373 0 0 1 11.87 11.948l13.578 13.579a9.368 9.368 0 0 1 9.704 2.23 9.386 9.386 0 0 1-6.64 16.025 9.393 9.393 0 0 1-9.21-7.547 9.384 9.384 0 0 1 .526-5.416L65.697 45.817v33.33a9.385 9.385 0 0 1 2.48 15.053 9.386 9.386 0 0 1-15.311-3.046A9.388 9.388 0 0 1 54.9 80.923a9.378 9.378 0 0 1 3.078-2.052V45.235a9.336 9.336 0 0 1-3.078-2.047A9.4 9.4 0 0 1 52.88 32.92l-13.89-13.89L2.311 55.703a7.89 7.89 0 0 0 0 11.16l53.495 53.497a7.895 7.895 0 0 0 11.157 0l53.244-53.245a7.9 7.9 0 0 0 0-11.162Z"
|
||||
/>
|
||||
</svg>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex md:hidden">
|
||||
<ThemeToggle />
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<span className="sr-only">Menu</span>
|
||||
<Menu className="w-5 h-5" />
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
<Logo width={120} height={40} />
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav
|
||||
id="mobile-menu"
|
||||
className="flex flex-col justify-center items-center gap-2 mt-4"
|
||||
>
|
||||
{NAVBAR.centerLinks?.map(({ href = "", label, target }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={target}
|
||||
className={buttonVariants({ variant: "ghost" })}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
{NAVBAR.rightLinks?.map(({ href, label, target }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={target}
|
||||
className={`w-[110px] gap-2 ${buttonVariants({
|
||||
variant: "secondary"
|
||||
})}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 123 123"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M120.208 55.953 66.715 2.463a7.885 7.885 0 0 0-11.158 0l-11.109 11.11 14.088 14.088a9.373 9.373 0 0 1 11.87 11.948l13.578 13.579a9.368 9.368 0 0 1 9.704 2.23 9.386 9.386 0 0 1-6.64 16.025 9.393 9.393 0 0 1-9.21-7.547 9.384 9.384 0 0 1 .526-5.416L65.697 45.817v33.33a9.385 9.385 0 0 1 2.48 15.053 9.386 9.386 0 0 1-15.311-3.046A9.388 9.388 0 0 1 54.9 80.923a9.378 9.378 0 0 1 3.078-2.052V45.235a9.336 9.336 0 0 1-3.078-2.047A9.4 9.4 0 0 1 52.88 32.92l-13.89-13.89L2.311 55.703a7.89 7.89 0 0 0 0 11.16l53.495 53.497a7.895 7.895 0 0 0 11.157 0l53.244-53.245a7.9 7.9 0 0 0 0-11.162Z"
|
||||
/>
|
||||
</svg>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileNav;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuList,
|
||||
NavigationMenu,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuList
|
||||
} from "@radix-ui/react-navigation-menu";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
@ -15,76 +15,76 @@ import { usePathname } from "next/navigation";
|
|||
import Logo from "./Logo";
|
||||
|
||||
const Navbar = () => {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<header className="sticky border-b top-0 z-40 w-full shadow-md bg-white dark:border-b-slate-800 dark:bg-background">
|
||||
{/* LOGO LEFT NAVBAR */}
|
||||
<NavigationMenu className="mx-auto">
|
||||
<NavigationMenuList className="container h-16 px-4 w-full flex justify-between items-center">
|
||||
<NavigationMenuItem className="font-bold flex items-center">
|
||||
<Link href="/#" className="inline-flex items-center gap-2">
|
||||
<span className="sr-only">SVR.JS</span>
|
||||
<Logo width={120} height={40} />
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<header className="sticky border-b top-0 z-40 w-full shadow-md bg-white dark:border-b-slate-800 dark:bg-background">
|
||||
{/* LOGO LEFT NAVBAR */}
|
||||
<NavigationMenu className="mx-auto">
|
||||
<NavigationMenuList className="container h-16 px-4 w-full flex justify-between items-center">
|
||||
<NavigationMenuItem className="font-bold flex items-center">
|
||||
<Link href="/#" className="inline-flex items-center gap-2">
|
||||
<span className="sr-only">SVR.JS</span>
|
||||
<Logo width={120} height={40} />
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* Mobile view */}
|
||||
<NavigationMenuItem className="flex md:hidden">
|
||||
<MobileNav />
|
||||
</NavigationMenuItem>
|
||||
{/* Mobile view */}
|
||||
<NavigationMenuItem className="flex md:hidden">
|
||||
<MobileNav />
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* Desktop Menu */}
|
||||
<NavigationMenuItem className="hidden md:flex">
|
||||
<nav className="hidden md:flex gap-4">
|
||||
{NAVBAR.centerLinks?.map(({ href, label, target }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={target}
|
||||
className={`text-[17px] tracking-tight ${
|
||||
pathname == href ? "bg-muted-foreground/20" : ""
|
||||
} ${buttonVariants({
|
||||
variant: "ghost",
|
||||
})}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</NavigationMenuItem>
|
||||
{/* Desktop Menu */}
|
||||
<NavigationMenuItem className="hidden md:flex">
|
||||
<nav className="hidden md:flex gap-4">
|
||||
{NAVBAR.centerLinks?.map(({ href, label, target }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={target}
|
||||
className={`text-[17px] tracking-tight ${
|
||||
pathname == href ? "bg-muted-foreground/20" : ""
|
||||
} ${buttonVariants({
|
||||
variant: "ghost"
|
||||
})}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem className="hidden md:flex gap-2 items-center">
|
||||
{NAVBAR.rightLinks?.map(({ href = "", label, target }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={target}
|
||||
className={`border ${buttonVariants({
|
||||
variant: "ghost",
|
||||
size: "icon",
|
||||
})}`}
|
||||
>
|
||||
<span className="sr-only">Git</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 123 123"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M120.208 55.953 66.715 2.463a7.885 7.885 0 0 0-11.158 0l-11.109 11.11 14.088 14.088a9.373 9.373 0 0 1 11.87 11.948l13.578 13.579a9.368 9.368 0 0 1 9.704 2.23 9.386 9.386 0 0 1-6.64 16.025 9.393 9.393 0 0 1-9.21-7.547 9.384 9.384 0 0 1 .526-5.416L65.697 45.817v33.33a9.385 9.385 0 0 1 2.48 15.053 9.386 9.386 0 0 1-15.311-3.046A9.388 9.388 0 0 1 54.9 80.923a9.378 9.378 0 0 1 3.078-2.052V45.235a9.336 9.336 0 0 1-3.078-2.047A9.4 9.4 0 0 1 52.88 32.92l-13.89-13.89L2.311 55.703a7.89 7.89 0 0 0 0 11.16l53.495 53.497a7.895 7.895 0 0 0 11.157 0l53.244-53.245a7.9 7.9 0 0 0 0-11.162Z"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</header>
|
||||
);
|
||||
<NavigationMenuItem className="hidden md:flex gap-2 items-center">
|
||||
{NAVBAR.rightLinks?.map(({ href = "", label, target }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={target}
|
||||
className={`border ${buttonVariants({
|
||||
variant: "ghost",
|
||||
size: "icon"
|
||||
})}`}
|
||||
>
|
||||
<span className="sr-only">Git</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 123 123"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M120.208 55.953 66.715 2.463a7.885 7.885 0 0 0-11.158 0l-11.109 11.11 14.088 14.088a9.373 9.373 0 0 1 11.87 11.948l13.578 13.579a9.368 9.368 0 0 1 9.704 2.23 9.386 9.386 0 0 1-6.64 16.025 9.393 9.393 0 0 1-9.21-7.547 9.384 9.384 0 0 1 .526-5.416L65.697 45.817v33.33a9.385 9.385 0 0 1 2.48 15.053 9.386 9.386 0 0 1-15.311-3.046A9.388 9.388 0 0 1 54.9 80.923a9.378 9.378 0 0 1 3.078-2.052V45.235a9.336 9.336 0 0 1-3.078-2.047A9.4 9.4 0 0 1 52.88 32.92l-13.89-13.89L2.311 55.703a7.89 7.89 0 0 0 0 11.16l53.495 53.497a7.895 7.895 0 0 0 11.157 0l53.244-53.245a7.9 7.9 0 0 0 0-11.162Z"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
|
|
@ -9,138 +9,138 @@ import { Mail } from "lucide-react";
|
|||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
const happyMonkey = Happy_Monkey({
|
||||
preload: true,
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
preload: true,
|
||||
weight: "400",
|
||||
subsets: ["latin"]
|
||||
});
|
||||
|
||||
const Newsletter = () => {
|
||||
const [submission, setSubmission] = useState<
|
||||
"idle" | "loading" | "success" | "error"
|
||||
>("idle");
|
||||
const [input, setInput] = useState<string>("");
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||
const [showCaptcha, setShowCaptcha] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Added this line
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const hcaptchaRef = useRef<HCaptcha>(null);
|
||||
const [submission, setSubmission] = useState<
|
||||
"idle" | "loading" | "success" | "error"
|
||||
>("idle");
|
||||
const [input, setInput] = useState<string>("");
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||
const [showCaptcha, setShowCaptcha] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Added this line
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const hcaptchaRef = useRef<HCaptcha>(null);
|
||||
|
||||
const handleCaptcha = async (token: string) => {
|
||||
setCaptchaToken(token);
|
||||
setShowCaptcha(false);
|
||||
await handleSubmit(token);
|
||||
};
|
||||
const handleCaptcha = async (token: string) => {
|
||||
setCaptchaToken(token);
|
||||
setShowCaptcha(false);
|
||||
await handleSubmit(token);
|
||||
};
|
||||
|
||||
const handleSubmit = async (token: string | null) => {
|
||||
if (!input || !token || isSubmitting) return;
|
||||
const handleSubmit = async (token: string | null) => {
|
||||
if (!input || !token || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmission("loading");
|
||||
setIsSubmitting(true);
|
||||
setSubmission("loading");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/subscribe", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email: input, captchaToken: token }),
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/subscribe", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ email: input, captchaToken: token })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setSubmission("success");
|
||||
setInput("");
|
||||
} else {
|
||||
setSubmission("error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error subscribing:", error);
|
||||
setSubmission("error");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
if (response.ok) {
|
||||
setSubmission("success");
|
||||
setInput("");
|
||||
} else {
|
||||
setSubmission("error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error subscribing:", error);
|
||||
setSubmission("error");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribeClick = () => {
|
||||
if (!input) return;
|
||||
setShowCaptcha(true);
|
||||
};
|
||||
const handleSubscribeClick = () => {
|
||||
if (!input) return;
|
||||
setShowCaptcha(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="newsletter">
|
||||
<hr className="w-11/12 mx-auto" />
|
||||
<div className="container py-24 md:py-32">
|
||||
<h3 className="text-center text-4xl md:text-5xl md:pb-2 text-black font-bold dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Join The Newsletter!
|
||||
</h3>
|
||||
<p className="text-lg text-muted-foreground text-center mt-4 md:mt-2 mb-8">
|
||||
Subscribe to our newsletter for updates. We promise no spam emails
|
||||
will be sent
|
||||
</p>
|
||||
<form
|
||||
className="relative flex flex-col w-full md:flex-row md:w-6/12 lg:w-4/12 mx-auto gap-4 md:gap-2"
|
||||
aria-label="Email Information"
|
||||
onSubmit={(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">
|
||||
<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
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Email address"
|
||||
required
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
onClick={handleSubscribeClick}
|
||||
disabled={submission === "loading" || !input || isSubmitting}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
return (
|
||||
<section id="newsletter">
|
||||
<hr className="w-11/12 mx-auto" />
|
||||
<div className="container py-24 md:py-32">
|
||||
<h3 className="text-center text-4xl md:text-5xl md:pb-2 text-black font-bold dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||
Join The Newsletter!
|
||||
</h3>
|
||||
<p className="text-lg text-muted-foreground text-center mt-4 md:mt-2 mb-8">
|
||||
Subscribe to our newsletter for updates. We promise no spam emails
|
||||
will be sent
|
||||
</p>
|
||||
<form
|
||||
className="relative flex flex-col w-full md:flex-row md:w-6/12 lg:w-4/12 mx-auto gap-4 md:gap-2"
|
||||
aria-label="Email Information"
|
||||
onSubmit={(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">
|
||||
<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
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Email address"
|
||||
required
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
onClick={handleSubscribeClick}
|
||||
disabled={submission === "loading" || !input || isSubmitting}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
|
||||
<div className="pointer-events-none dark:invert -scale-x-100 absolute -bottom-14 right-1/2 md:right-14 inline-flex justify-center items-center gap-1">
|
||||
<Image
|
||||
src="/curly-arrow.png"
|
||||
alt="see here"
|
||||
width={35}
|
||||
height={35}
|
||||
/>
|
||||
<div className="pointer-events-none dark:invert -scale-x-100 absolute -bottom-14 right-1/2 md:right-14 inline-flex justify-center items-center gap-1">
|
||||
<Image
|
||||
src="/curly-arrow.png"
|
||||
alt="see here"
|
||||
width={35}
|
||||
height={35}
|
||||
/>
|
||||
|
||||
<span
|
||||
className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`}
|
||||
>
|
||||
{submission === "idle" && "Subscribe Now"}
|
||||
{submission === "loading" && (
|
||||
<p className="text-sm text-center">Subscribing...</p>
|
||||
)}
|
||||
{submission === "success" && (
|
||||
<p className="dark:invert text-sm text-center text-green-500">
|
||||
🎉 Subscribed successfully...
|
||||
</p>
|
||||
)}
|
||||
{submission === "error" && (
|
||||
<p className="dark:invert text-sm text-center text-red-500">
|
||||
😥 Something went wrong...
|
||||
</p>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
{showCaptcha && (
|
||||
<div className="flex-center relative">
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
onVerify={handleCaptcha}
|
||||
ref={hcaptchaRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<hr className="w-11/12 mx-auto" />
|
||||
</section>
|
||||
);
|
||||
<span
|
||||
className={`mt-10 font-bold text-black -scale-x-100 text-[15px] ${happyMonkey.className}`}
|
||||
>
|
||||
{submission === "idle" && "Subscribe Now"}
|
||||
{submission === "loading" && (
|
||||
<p className="text-sm text-center">Subscribing...</p>
|
||||
)}
|
||||
{submission === "success" && (
|
||||
<p className="dark:invert text-sm text-center text-green-500">
|
||||
🎉 Subscribed successfully...
|
||||
</p>
|
||||
)}
|
||||
{submission === "error" && (
|
||||
<p className="dark:invert text-sm text-center text-red-500">
|
||||
😥 Something went wrong...
|
||||
</p>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
{showCaptcha && (
|
||||
<div className="flex-center relative">
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
onVerify={handleCaptcha}
|
||||
ref={hcaptchaRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<hr className="w-11/12 mx-auto" />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Newsletter;
|
||||
|
|
|
@ -7,50 +7,50 @@ import { useRouter } from "next/navigation";
|
|||
import HeroVideoDialog from "../ui/heroVideoAction";
|
||||
|
||||
const Partners = () => {
|
||||
const router = useRouter();
|
||||
const handleClick = () => {
|
||||
router.push("/docs");
|
||||
};
|
||||
return (
|
||||
<section
|
||||
id="partners"
|
||||
className="wrapper container py-24 md:py-28 gap-4 flex flex-col"
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-start">
|
||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||
SVRJS
|
||||
</span>{" "}
|
||||
in action
|
||||
</h2>
|
||||
<div className="w-full flex-start flex-row">
|
||||
<div className="flex max-md:flex-col items-center justify-start gap-4">
|
||||
<h2 className="text-md font-medium bg-accent/60 px-2 py-2 rounded-md">
|
||||
Process of setting up a WordPress website running on SVR.JS.
|
||||
</h2>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
className="flex-center font-bold max-md:w-full max-w-xl"
|
||||
>
|
||||
Docs <ArrowUpRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
const router = useRouter();
|
||||
const handleClick = () => {
|
||||
router.push("/docs");
|
||||
};
|
||||
return (
|
||||
<section
|
||||
id="partners"
|
||||
className="wrapper container py-24 md:py-28 gap-4 flex flex-col"
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-start">
|
||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||
SVRJS
|
||||
</span>{" "}
|
||||
in action
|
||||
</h2>
|
||||
<div className="w-full flex-start flex-row">
|
||||
<div className="flex max-md:flex-col items-center justify-start gap-4">
|
||||
<h2 className="text-md font-medium bg-accent/60 px-2 py-2 rounded-md">
|
||||
Process of setting up a WordPress website running on SVR.JS.
|
||||
</h2>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
className="flex-center font-bold max-md:w-full max-w-xl"
|
||||
>
|
||||
Docs <ArrowUpRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeroVideoDialog
|
||||
animationStyle="top-in-bottom-out"
|
||||
videoSrc="https://odysee.com/$/embed/@SVRJS:5/svrjs-in-action:e?r=7t9EG6VDTNZDSze8ysoChqocLNhAMZEe"
|
||||
thumbnailSrc="/poster.svg"
|
||||
thumbnailAlt="Poster.svg"
|
||||
/>
|
||||
{/* <video
|
||||
<HeroVideoDialog
|
||||
animationStyle="top-in-bottom-out"
|
||||
videoSrc="https://odysee.com/$/embed/@SVRJS:5/svrjs-in-action:e?r=7t9EG6VDTNZDSze8ysoChqocLNhAMZEe"
|
||||
thumbnailSrc="/poster.svg"
|
||||
thumbnailAlt="Poster.svg"
|
||||
/>
|
||||
{/* <video
|
||||
src="/svgaction.mp4"
|
||||
className="rounded-xl aspect-video bg-[#09090b]"
|
||||
controls
|
||||
poster="/poster.svg"
|
||||
></video> */}
|
||||
<hr className="w-full h-1" />
|
||||
</section>
|
||||
);
|
||||
<hr className="w-full h-1" />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Partners;
|
||||
|
|
|
@ -2,20 +2,20 @@ import { stats } from "@/constants";
|
|||
import NumberTicker from "../widgets/num-tick";
|
||||
|
||||
const Statistics = () => {
|
||||
return (
|
||||
<section>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{stats.map(({ title, count }) => (
|
||||
<div key={title} className="space-y-2 text-center">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold">
|
||||
<NumberTicker value={count} />
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground">{title}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{stats.map(({ title, count }) => (
|
||||
<div key={title} className="space-y-2 text-center">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold">
|
||||
<NumberTicker value={count} />
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground">{title}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Statistics;
|
||||
|
|
|
@ -2,104 +2,104 @@ import React from "react";
|
|||
import TestimonialCard from "../cards/testimonialCard";
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: "John Doe",
|
||||
role: "CEO, Example Corp.",
|
||||
avatar: "avatar1",
|
||||
testimonial:
|
||||
"Working with this team was a fantastic experience. They developed our website exactly to our specifications, and everything was seamless and well-integrated.",
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
name: "Jane Smith",
|
||||
role: "CEO, CleanCo",
|
||||
avatar: "avatar2",
|
||||
testimonial:
|
||||
"We're thrilled with the website. It's simple, clean, and has significantly boosted our sales. The developers did an excellent job.",
|
||||
rating: 4,
|
||||
},
|
||||
{
|
||||
name: "Sam Green",
|
||||
role: "Web Developer",
|
||||
avatar: "avatar3",
|
||||
testimonial:
|
||||
"Collaborating with this team to build a SaaS-integrated website was a perfect experience. I look forward to working with them again.",
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
name: "Chris Brown",
|
||||
role: "Web Coder",
|
||||
avatar: "avatar4",
|
||||
testimonial:
|
||||
"The team's understanding of our needs and their ability to provide fitting solutions was impressive. Their support and guidance were invaluable.",
|
||||
rating: 4,
|
||||
},
|
||||
{
|
||||
name: "Alex Johnson",
|
||||
avatar: "avatar5",
|
||||
testimonial:
|
||||
"Exceptional service and outstanding results. They consistently deliver on time and within budget, making them our go-to partner for all our projects.",
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
name: "Patricia Taylor",
|
||||
role: "Web Developer",
|
||||
avatar: "avatar6",
|
||||
testimonial:
|
||||
"It was great to work with them. I needed a design for a SaaS project, and it was delivered within 2 days.",
|
||||
rating: 4,
|
||||
},
|
||||
{
|
||||
name: "Emily Davis",
|
||||
role: "UX Designer, Creative Agency",
|
||||
avatar: "avatar7",
|
||||
testimonial:
|
||||
"Collaborating with them has been a pleasure. Their creativity and user-centric approach have significantly enhanced our product's usability.",
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
name: "Michael Lee",
|
||||
avatar: "avatar8",
|
||||
testimonial:
|
||||
"They have a keen understanding of our business needs and consistently deliver top-notch solutions. Their reliability and efficiency are commendable.",
|
||||
rating: 5,
|
||||
},
|
||||
{
|
||||
name: "Sarah Wilson",
|
||||
avatar: "avatar9",
|
||||
testimonial:
|
||||
"Their dedication to client satisfaction is evident in everything they do. We've seen remarkable improvements in our processes thanks to their expertise.",
|
||||
rating: 4,
|
||||
},
|
||||
{
|
||||
name: "John Doe",
|
||||
role: "CEO, Example Corp.",
|
||||
avatar: "avatar1",
|
||||
testimonial:
|
||||
"Working with this team was a fantastic experience. They developed our website exactly to our specifications, and everything was seamless and well-integrated.",
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: "Jane Smith",
|
||||
role: "CEO, CleanCo",
|
||||
avatar: "avatar2",
|
||||
testimonial:
|
||||
"We're thrilled with the website. It's simple, clean, and has significantly boosted our sales. The developers did an excellent job.",
|
||||
rating: 4
|
||||
},
|
||||
{
|
||||
name: "Sam Green",
|
||||
role: "Web Developer",
|
||||
avatar: "avatar3",
|
||||
testimonial:
|
||||
"Collaborating with this team to build a SaaS-integrated website was a perfect experience. I look forward to working with them again.",
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: "Chris Brown",
|
||||
role: "Web Coder",
|
||||
avatar: "avatar4",
|
||||
testimonial:
|
||||
"The team's understanding of our needs and their ability to provide fitting solutions was impressive. Their support and guidance were invaluable.",
|
||||
rating: 4
|
||||
},
|
||||
{
|
||||
name: "Alex Johnson",
|
||||
avatar: "avatar5",
|
||||
testimonial:
|
||||
"Exceptional service and outstanding results. They consistently deliver on time and within budget, making them our go-to partner for all our projects.",
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: "Patricia Taylor",
|
||||
role: "Web Developer",
|
||||
avatar: "avatar6",
|
||||
testimonial:
|
||||
"It was great to work with them. I needed a design for a SaaS project, and it was delivered within 2 days.",
|
||||
rating: 4
|
||||
},
|
||||
{
|
||||
name: "Emily Davis",
|
||||
role: "UX Designer, Creative Agency",
|
||||
avatar: "avatar7",
|
||||
testimonial:
|
||||
"Collaborating with them has been a pleasure. Their creativity and user-centric approach have significantly enhanced our product's usability.",
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: "Michael Lee",
|
||||
avatar: "avatar8",
|
||||
testimonial:
|
||||
"They have a keen understanding of our business needs and consistently deliver top-notch solutions. Their reliability and efficiency are commendable.",
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: "Sarah Wilson",
|
||||
avatar: "avatar9",
|
||||
testimonial:
|
||||
"Their dedication to client satisfaction is evident in everything they do. We've seen remarkable improvements in our processes thanks to their expertise.",
|
||||
rating: 4
|
||||
}
|
||||
];
|
||||
|
||||
const Testimonials = () => {
|
||||
return (
|
||||
<section className="mx-auto flex w-full max-w-7xl flex-col pt-12 md:pt-24">
|
||||
<div className="flex flex-row items-center justify-center space-x-1">
|
||||
<span className="text-white/50 text-xs lg:text-base">Testimonials</span>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-5xl font-bold text-center">
|
||||
Hear it from{" "}
|
||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||
our users
|
||||
</span>
|
||||
</h1>
|
||||
return (
|
||||
<section className="mx-auto flex w-full max-w-7xl flex-col pt-12 md:pt-24">
|
||||
<div className="flex flex-row items-center justify-center space-x-1">
|
||||
<span className="text-white/50 text-xs lg:text-base">Testimonials</span>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-5xl font-bold text-center">
|
||||
Hear it from{" "}
|
||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||
our users
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<ul className="wrapper columns-1 gap-5 md:columns-2 lg:columns-3 py-6 mt-6">
|
||||
{testimonials.map((testimonial, idx) => (
|
||||
<TestimonialCard
|
||||
avatar={testimonial.avatar}
|
||||
name={testimonial.name}
|
||||
role={testimonial.role}
|
||||
testimonial={testimonial.testimonial}
|
||||
rating={testimonial.rating}
|
||||
key={idx}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
<ul className="wrapper columns-1 gap-5 md:columns-2 lg:columns-3 py-6 mt-6">
|
||||
{testimonials.map((testimonial, idx) => (
|
||||
<TestimonialCard
|
||||
avatar={testimonial.avatar}
|
||||
name={testimonial.name}
|
||||
role={testimonial.role}
|
||||
testimonial={testimonial.testimonial}
|
||||
rating={testimonial.rating}
|
||||
key={idx}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Testimonials;
|
||||
|
|
|
@ -5,25 +5,25 @@ import { Button } from "../ui/button";
|
|||
import { Check, Copy } from "lucide-react";
|
||||
|
||||
export default function CopyButton({ code }: { code: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy!", error);
|
||||
}
|
||||
};
|
||||
const copyCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy!", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={copyCode}
|
||||
className="absolute top-2 right-2 bg-accent hover:bg-muted text-white p-2 rounded"
|
||||
size={"icon"}
|
||||
>
|
||||
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
onClick={copyCode}
|
||||
className="absolute top-2 right-2 bg-accent hover:bg-muted text-white p-2 rounded"
|
||||
size={"icon"}
|
||||
>
|
||||
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { SessionProvider } from "next-auth/react";
|
||||
|
||||
export default function AuthProvider({
|
||||
children,
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React from "react";
|
||||
|
||||
interface ChangelogLayoutProps {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ChangelogLayout: React.FC<ChangelogLayoutProps> = ({
|
||||
children,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<div className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||
<div className="prose max-w-full prose-lg dark:prose-invert">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
||||
<div className="prose max-w-full prose-lg dark:prose-invert">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
|
|
|
@ -9,48 +9,48 @@ import { cn } from "@/lib/utils";
|
|||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-45",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<PlusIcon className="h-5 w-5 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-45",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<PlusIcon className="h-5 w-5 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ReactNode } from "react";
|
|||
|
||||
export default function AnimatedGradientText({
|
||||
children,
|
||||
className,
|
||||
className
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
|
@ -12,7 +12,7 @@ export default function AnimatedGradientText({
|
|||
<div
|
||||
className={cn(
|
||||
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -10,13 +10,13 @@ interface AnimatedShinyTextProps {
|
|||
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||
children,
|
||||
className,
|
||||
shimmerWidth = 100,
|
||||
shimmerWidth = 100
|
||||
}) => {
|
||||
return (
|
||||
<p
|
||||
style={
|
||||
{
|
||||
"--shimmer-width": `${shimmerWidth}px`,
|
||||
"--shimmer-width": `${shimmerWidth}px`
|
||||
} as CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
|
@ -28,7 +28,7 @@ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
|||
// Shimmer gradient
|
||||
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
|
||||
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -13,7 +13,7 @@ const Avatar = React.forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -40,7 +40,7 @@ const AvatarFallback = React.forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -14,13 +14,13 @@ const badgeVariants = cva(
|
|||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
outline: "text-foreground"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
|
|
|
@ -19,7 +19,7 @@ export const BorderBeam = ({
|
|||
borderWidth = 1.5,
|
||||
colorFrom = "#8803AF",
|
||||
colorTo = "#61DAFB",
|
||||
delay = 0,
|
||||
delay = 0
|
||||
}: BorderBeamProps) => {
|
||||
return (
|
||||
<div
|
||||
|
@ -31,7 +31,7 @@ export const BorderBeam = ({
|
|||
"--border-width": borderWidth,
|
||||
"--color-from": colorFrom,
|
||||
"--color-to": colorTo,
|
||||
"--delay": `-${delay}s`,
|
||||
"--delay": `-${delay}s`
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
|
@ -17,40 +17,40 @@ const buttonVariants = cva(
|
|||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
link: "text-primary underline-offset-4 hover:underline"
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
icon: "h-10 w-10"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
|
|
@ -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<
|
||||
HTMLDivElement,
|
||||
|
@ -14,8 +14,8 @@ const Card = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
|
|||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
|
@ -41,8 +41,8 @@ const CardTitle = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
|
@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
|
|||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
|
|||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent
|
||||
};
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
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<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
|
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
|
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
|
|||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
|
@ -64,8 +64,8 @@ const DialogHeader = ({
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
|
@ -78,8 +78,8 @@ const DialogFooter = ({
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
|
@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
|
@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
|
|||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
|
@ -118,5 +118,5 @@ export {
|
|||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
DialogDescription
|
||||
};
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
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<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
|
@ -36,9 +36,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
|
@ -52,9 +52,9 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
|
@ -71,13 +71,13 @@ const DropdownMenuContent = React.forwardRef<
|
|||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
|
@ -89,8 +89,8 @@ const DropdownMenuItem = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
|
@ -112,9 +112,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
|
@ -135,13 +135,13 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
|
@ -153,8 +153,8 @@ const DropdownMenuLabel = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
|
@ -165,8 +165,8 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
|
@ -177,9 +177,9 @@ const DropdownMenuShortcut = ({
|
|||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
|
@ -196,5 +196,5 @@ export {
|
|||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
DropdownMenuRadioGroup
|
||||
};
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
useFormContext
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const Form = FormProvider
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
|
@ -36,21 +36,21 @@ const FormField = <
|
|||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
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 {
|
||||
id,
|
||||
|
@ -58,37 +58,37 @@ const useFormField = () => {
|
|||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
...fieldState
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
|
@ -97,15 +97,16 @@ const FormLabel = React.forwardRef<
|
|||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
|
@ -119,15 +120,15 @@ const FormControl = React.forwardRef<
|
|||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
|
@ -136,19 +137,19 @@ const FormDescription = React.forwardRef<
|
|||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -160,9 +161,9 @@ const FormMessage = React.forwardRef<
|
|||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
|
@ -172,5 +173,5 @@ export {
|
|||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
FormField
|
||||
};
|
||||
|
|
|
@ -29,7 +29,7 @@ export function GridPattern({
|
|||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/35 stroke-gray-400/35",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
@ -6,151 +6,151 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||
import { Play, XIcon } from "lucide-react";
|
||||
|
||||
type AnimationStyle =
|
||||
| "from-bottom"
|
||||
| "from-center"
|
||||
| "from-top"
|
||||
| "from-left"
|
||||
| "from-right"
|
||||
| "fade"
|
||||
| "top-in-bottom-out"
|
||||
| "left-in-right-out";
|
||||
| "from-bottom"
|
||||
| "from-center"
|
||||
| "from-top"
|
||||
| "from-left"
|
||||
| "from-right"
|
||||
| "fade"
|
||||
| "top-in-bottom-out"
|
||||
| "left-in-right-out";
|
||||
|
||||
interface HeroVideoProps {
|
||||
animationStyle?: AnimationStyle;
|
||||
videoSrc: string;
|
||||
thumbnailSrc: string;
|
||||
thumbnailAlt?: string;
|
||||
animationStyle?: AnimationStyle;
|
||||
videoSrc: string;
|
||||
thumbnailSrc: string;
|
||||
thumbnailAlt?: string;
|
||||
}
|
||||
|
||||
const animationVariants = {
|
||||
"from-bottom": {
|
||||
initial: { y: "100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "100%", opacity: 0 },
|
||||
},
|
||||
"from-center": {
|
||||
initial: { scale: 0.5, opacity: 0 },
|
||||
animate: { scale: 1, opacity: 1 },
|
||||
exit: { scale: 0.5, opacity: 0 },
|
||||
},
|
||||
"from-top": {
|
||||
initial: { y: "-100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "-100%", opacity: 0 },
|
||||
},
|
||||
"from-left": {
|
||||
initial: { x: "-100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "-100%", opacity: 0 },
|
||||
},
|
||||
"from-right": {
|
||||
initial: { x: "100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "100%", opacity: 0 },
|
||||
},
|
||||
fade: {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
},
|
||||
"top-in-bottom-out": {
|
||||
initial: { y: "-100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "100%", opacity: 0 },
|
||||
},
|
||||
"left-in-right-out": {
|
||||
initial: { x: "-100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "100%", opacity: 0 },
|
||||
},
|
||||
"from-bottom": {
|
||||
initial: { y: "100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "100%", opacity: 0 }
|
||||
},
|
||||
"from-center": {
|
||||
initial: { scale: 0.5, opacity: 0 },
|
||||
animate: { scale: 1, opacity: 1 },
|
||||
exit: { scale: 0.5, opacity: 0 }
|
||||
},
|
||||
"from-top": {
|
||||
initial: { y: "-100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "-100%", opacity: 0 }
|
||||
},
|
||||
"from-left": {
|
||||
initial: { x: "-100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "-100%", opacity: 0 }
|
||||
},
|
||||
"from-right": {
|
||||
initial: { x: "100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "100%", opacity: 0 }
|
||||
},
|
||||
fade: {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 }
|
||||
},
|
||||
"top-in-bottom-out": {
|
||||
initial: { y: "-100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "100%", opacity: 0 }
|
||||
},
|
||||
"left-in-right-out": {
|
||||
initial: { x: "-100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "100%", opacity: 0 }
|
||||
}
|
||||
};
|
||||
|
||||
export default function HeroVideoDialog({
|
||||
animationStyle = "from-center",
|
||||
videoSrc,
|
||||
thumbnailSrc,
|
||||
thumbnailAlt = "Video thumbnail",
|
||||
animationStyle = "from-center",
|
||||
videoSrc,
|
||||
thumbnailSrc,
|
||||
thumbnailAlt = "Video thumbnail"
|
||||
}: HeroVideoProps) {
|
||||
const [isVideoOpen, setIsVideoOpen] = useState(false);
|
||||
const [isCloseHovered, setIsCloseHovered] = useState(false);
|
||||
const [isPlayHovered, setIsPlayHovered] = useState(false);
|
||||
const [isVideoOpen, setIsVideoOpen] = useState(false);
|
||||
const [isCloseHovered, setIsCloseHovered] = useState(false);
|
||||
const [isPlayHovered, setIsPlayHovered] = useState(false);
|
||||
|
||||
const openVideo = () => setIsVideoOpen(true);
|
||||
const closeVideo = () => setIsVideoOpen(false);
|
||||
const openVideo = () => setIsVideoOpen(true);
|
||||
const closeVideo = () => setIsVideoOpen(false);
|
||||
|
||||
const selectedAnimation = animationVariants[animationStyle];
|
||||
const selectedAnimation = animationVariants[animationStyle];
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative cursor-pointer" onClick={openVideo}>
|
||||
<img
|
||||
src={thumbnailSrc}
|
||||
alt={thumbnailAlt}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="w-full rounded-2xl"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div
|
||||
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)}
|
||||
onMouseLeave={() => setIsPlayHovered(false)}
|
||||
>
|
||||
<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 ${
|
||||
isPlayHovered ? " scale-105" : "scale-100"
|
||||
}`}
|
||||
>
|
||||
<Play
|
||||
className="size-8 text-white"
|
||||
style={{
|
||||
transform: isPlayHovered ? "scale(1.1)" : "scale(1)",
|
||||
transition: "transform 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{isVideoOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
|
||||
>
|
||||
<motion.div
|
||||
{...selectedAnimation}
|
||||
transition={{ type: "spring", damping: 30, stiffness: 300 }}
|
||||
className="relative w-full max-w-4xl aspect-video mx-4 md:mx-0"
|
||||
>
|
||||
<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"
|
||||
onClick={closeVideo}
|
||||
onHoverStart={() => setIsCloseHovered(true)}
|
||||
onHoverEnd={() => setIsCloseHovered(false)}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
</motion.button>
|
||||
<motion.div
|
||||
className="size-full border-2 border-white rounded-2xl overflow-hidden isolate z-[1] relative"
|
||||
animate={{ scale: isCloseHovered ? 0.98 : 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<iframe
|
||||
src={videoSrc}
|
||||
className="size-full rounded-2xl"
|
||||
allowFullScreen
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
></iframe>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative cursor-pointer" onClick={openVideo}>
|
||||
<img
|
||||
src={thumbnailSrc}
|
||||
alt={thumbnailAlt}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="w-full rounded-2xl"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div
|
||||
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)}
|
||||
onMouseLeave={() => setIsPlayHovered(false)}
|
||||
>
|
||||
<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 ${
|
||||
isPlayHovered ? " scale-105" : "scale-100"
|
||||
}`}
|
||||
>
|
||||
<Play
|
||||
className="size-8 text-white"
|
||||
style={{
|
||||
transform: isPlayHovered ? "scale(1.1)" : "scale(1)",
|
||||
transition: "transform 0.3s ease"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{isVideoOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
|
||||
>
|
||||
<motion.div
|
||||
{...selectedAnimation}
|
||||
transition={{ type: "spring", damping: 30, stiffness: 300 }}
|
||||
className="relative w-full max-w-4xl aspect-video mx-4 md:mx-0"
|
||||
>
|
||||
<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"
|
||||
onClick={closeVideo}
|
||||
onHoverStart={() => setIsCloseHovered(true)}
|
||||
onHoverEnd={() => setIsCloseHovered(false)}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
</motion.button>
|
||||
<motion.div
|
||||
className="size-full border-2 border-white rounded-2xl overflow-hidden isolate z-[1] relative"
|
||||
animate={{ scale: isCloseHovered ? 0.98 : 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<iframe
|
||||
src={videoSrc}
|
||||
className="size-full rounded-2xl"
|
||||
allowFullScreen
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
></iframe>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue