Compare commits
No commits in common. "proxy" and "main" have entirely different histories.
257 changed files with 31687 additions and 13094 deletions
25
.env.example
25
.env.example
|
@ -1,4 +1,5 @@
|
||||||
MONGODB_URI=
|
MONGODB_URI=
|
||||||
|
MONGODB_DB=
|
||||||
|
|
||||||
UPLOADTHING_SECRET=
|
UPLOADTHING_SECRET=
|
||||||
UPLOADTHING_APP_ID=
|
UPLOADTHING_APP_ID=
|
||||||
|
@ -7,6 +8,26 @@ ADMIN_USERNAME=
|
||||||
ADMIN_PASSWORD=
|
ADMIN_PASSWORD=
|
||||||
|
|
||||||
NEXTAUTH_SECRET=
|
NEXTAUTH_SECRET=
|
||||||
|
NEXTAUTH_URL=
|
||||||
|
|
||||||
EMAIL=
|
NEXT_PUBLIC_WEBSITE_URL=
|
||||||
EMAIL_PASS=
|
|
||||||
|
EMAIL_SERVER=
|
||||||
|
EMAIL_PORT=
|
||||||
|
EMAIL_SECURE=
|
||||||
|
EMAIL_USER=
|
||||||
|
EMAIL_PASS=
|
||||||
|
EMAIL_NEWSLETTER_ADDRESS=
|
||||||
|
EMAIL_NEWSLETTER_TESTDEST=
|
||||||
|
EMAIL_CONTACT_ADDRESS=
|
||||||
|
EMAIL_CONTACT_DEST=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_SANITY_PROJECT_ID=
|
||||||
|
SANITY_AUTH_TOKEN=
|
||||||
|
SANITY_WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_MATOMO_URL=
|
||||||
|
NEXT_PUBLIC_MATOMO_SITE_ID=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=
|
||||||
|
HCAPTCHA_SECRET=
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"extends": "next/core-web-vitals"
|
"extends": ["next/core-web-vitals", "prettier", "plugin:prettier/recommended"]
|
||||||
}
|
}
|
||||||
|
|
13
.forgejo/workflows/deploy.yaml
Normal file
13
.forgejo/workflows/deploy.yaml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
name: Deploy Next.js application
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy Next.js application
|
||||||
|
uses: https://github.com/appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.HOST }}
|
||||||
|
username: ${{ secrets.USERNAME }}
|
||||||
|
password: ${{ secrets.PASSWORD }}
|
||||||
|
script: deploy-next-app
|
19
.github/workflows/main.yml
vendored
Normal file
19
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Sync repo to the Codeberg mirror
|
||||||
|
name: Repo sync GitHub -> SVR.JS Git server
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
svrjsgit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: spyoungtech/mirror-action@v0.5.1
|
||||||
|
with:
|
||||||
|
REMOTE: "https://git.svrjs.org/svrjs/svrjs-nextjs-website.git"
|
||||||
|
GIT_USERNAME: github-mirror
|
||||||
|
GIT_PASSWORD: ${{ secrets.GIT_PASSWORD }}
|
2
.husky/commit-msg
Executable file
2
.husky/commit-msg
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
npx --no -- commitlint --edit "$1"
|
2
.husky/pre-commit
Executable file
2
.husky/pre-commit
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
npx lint-staged
|
7
.swcrc
Normal file
7
.swcrc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"jsc": {
|
||||||
|
"keepClassNames": true,
|
||||||
|
"target": "es2017"
|
||||||
|
},
|
||||||
|
"minify": true
|
||||||
|
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2024 SVR.JS
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -1,13 +0,0 @@
|
||||||
// 'use server';
|
|
||||||
// import { NextApiRequest } from 'next';
|
|
||||||
// import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
// export async function POST(req: NextApiRequest) {
|
|
||||||
// const { username, password } = await req.body;
|
|
||||||
|
|
||||||
// if (username === process.env.USERNAME && password === process.env.PASSWORD) {
|
|
||||||
// return NextResponse.json({ success: true });
|
|
||||||
// } else {
|
|
||||||
// return NextResponse.json({ success: false });
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -9,7 +9,7 @@ interface CardProps {
|
||||||
|
|
||||||
const Card: FC<CardProps> = ({ title, url }) => {
|
const Card: FC<CardProps> = ({ title, url }) => {
|
||||||
return (
|
return (
|
||||||
<div className=" bg-zinc-900 border rounded-lg hover:bg-zinc-800 transition-all">
|
<div className="bg-accent border rounded-lg hover:bg-muted transition-all">
|
||||||
<Link href={url} className="group">
|
<Link href={url} className="group">
|
||||||
<div className="flex-center rounded-lg p-6">
|
<div className="flex-center rounded-lg p-6">
|
||||||
<h2 className="text-2xl font-bold mb-2">{title}</h2>
|
<h2 className="text-2xl font-bold mb-2">{title}</h2>
|
||||||
|
|
|
@ -9,49 +9,49 @@ import { Menu } from "lucide-react";
|
||||||
import Logo from "@/components/shared/Logo";
|
import Logo from "@/components/shared/Logo";
|
||||||
|
|
||||||
const MobileNav = () => {
|
const MobileNav = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<Link href="/" className="flex items-center gap-2 md:py-2">
|
<Link href="/" className="flex items-center gap-2 md:py-2">
|
||||||
<Logo width={120} height={40} />
|
<Logo width={120} height={40} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="flex gap-2">
|
<nav className="flex gap-2">
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger>
|
<SheetTrigger>
|
||||||
<Menu className="w-5 h-5" />
|
<Menu className="w-5 h-5" />
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="sheet-content sm:w-64">
|
<SheetContent className="sheet-content sm:w-64">
|
||||||
<>
|
<>
|
||||||
<Logo width={155} height={53} />
|
<Logo width={155} height={53} />
|
||||||
<ul className="header-nav_elements">
|
<ul className="header-nav_elements">
|
||||||
{AdminLinks.slice(0, 6).map((link) => {
|
{AdminLinks.slice(0, 7).map((link) => {
|
||||||
const isActive = link.url === pathname;
|
const isActive = link.url === pathname;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={link.url}
|
key={link.url}
|
||||||
className={`${
|
className={`${
|
||||||
isActive && "gradient-text"
|
isActive && "gradient-text"
|
||||||
} p-18 flex whitespace-nowrap text-dark-700`}
|
} p-18 flex whitespace-nowrap text-dark-700`}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
className="sidebar-link cursor-pointer"
|
className="sidebar-link cursor-pointer"
|
||||||
href={link.url}
|
href={link.url}
|
||||||
>
|
>
|
||||||
<link.icon />
|
<link.icon />
|
||||||
{link.name}
|
{link.name}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MobileNav;
|
export default MobileNav;
|
||||||
|
|
|
@ -6,60 +6,60 @@ import { AdminLinks } from "@/constants";
|
||||||
import Logo from "@/components/shared/Logo";
|
import Logo from "@/components/shared/Logo";
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
<div className="flex size-full flex-col gap-4">
|
<div className="flex size-full flex-col gap-4">
|
||||||
<Link href="/" className="sidebar-logo">
|
<Link href="/" className="sidebar-logo">
|
||||||
<Logo width={155} height={53} />
|
<Logo width={155} height={53} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="sidebar-nav">
|
<nav className="sidebar-nav">
|
||||||
<ul className="sidebar-nav_elements">
|
<ul className="sidebar-nav_elements">
|
||||||
{AdminLinks.slice(0, 5).map((link) => {
|
{AdminLinks.slice(0, 7).map((link) => {
|
||||||
const isActive = link.url === pathname;
|
const isActive = link.url === pathname;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={link.url}
|
key={link.url}
|
||||||
className={`sidebar-nav_element group ${
|
className={`sidebar-nav_element group ${
|
||||||
isActive ? "bg-white/5" : "text-muted-foreground"
|
isActive ? "bg-white/5" : "text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Link className="sidebar-link" href={link.url}>
|
<Link className="sidebar-link" href={link.url}>
|
||||||
<link.icon />
|
<link.icon />
|
||||||
{link.name}
|
{link.name}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul className="sidebar-nav_elements">
|
<ul className="sidebar-nav_elements">
|
||||||
{AdminLinks.slice(5).map((link) => {
|
{AdminLinks.slice(7).map((link) => {
|
||||||
const isActive = link.url === pathname;
|
const isActive = link.url === pathname;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={link.url}
|
key={link.url}
|
||||||
className={`sidebar-nav_element group ${
|
className={`sidebar-nav_element group ${
|
||||||
isActive ? "bg-purple-gradient" : "text-muted-foreground"
|
isActive ? "bg-purple-gradient" : "text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Link className="sidebar-link" href={link.url}>
|
<Link className="sidebar-link" href={link.url}>
|
||||||
<link.icon />
|
<link.icon />
|
||||||
{link.name}
|
{link.name}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Admin // Changelogs",
|
title: "Admin // Changelogs"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,21 +5,21 @@ import { useForm, SubmitHandler, useFieldArray } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { logsSchema } from "@/lib/validations/validation";
|
import { logsSchema } from "@/lib/validations/validation";
|
||||||
|
@ -27,206 +27,206 @@ import { z } from "zod";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
_id: string;
|
_id: string;
|
||||||
version: string;
|
version: string;
|
||||||
date: string;
|
date: string;
|
||||||
bullets: { point: string }[];
|
bullets: { point: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogsFormValues = z.infer<typeof logsSchema>;
|
type LogsFormValues = z.infer<typeof logsSchema>;
|
||||||
|
|
||||||
const AdminLogPage = () => {
|
const AdminLogPage = () => {
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const form = useForm<LogsFormValues>({
|
const form = useForm<LogsFormValues>({
|
||||||
resolver: zodResolver(logsSchema),
|
resolver: zodResolver(logsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
version: "",
|
version: "",
|
||||||
date: "",
|
date: "",
|
||||||
bullets: [{ point: "" }],
|
bullets: [{ point: "" }]
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: "bullets",
|
name: "bullets"
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/logs", { method: "GET" });
|
const response = await fetch("/api/logs", { method: "GET" });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data: LogEntry[] = await response.json();
|
const data: LogEntry[] = await response.json();
|
||||||
setLogs(data);
|
setLogs(data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message || "Failed to fetch logs");
|
setError(error.message || "Failed to fetch logs");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
|
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch("/api/uploadlogs", {
|
const response = await fetch("/api/uploadlogs", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
form.reset();
|
form.reset();
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast({ description: "Logs successfully added" });
|
toast({ description: "Logs successfully added" });
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast({ description: "Upload Failed", variant: "destructive" });
|
toast({ description: "Upload Failed", variant: "destructive" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteLog = async (id: string) => {
|
const deleteLog = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/delete/logs/${id}`, {
|
const response = await fetch(`/api/delete/logs/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE"
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message || "Failed to delete log");
|
setError(error.message || "Failed to delete log");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="logs-page" className="wrapper container">
|
<section id="logs-page" className="wrapper container">
|
||||||
<h1 className="text-3xl font-bold py-6">Server Logs Form</h1>
|
<h1 className="text-3xl font-bold py-6">Server Logs Form</h1>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="version"
|
name="version"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Version Name</FormLabel>
|
<FormLabel>Version Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="date"
|
name="date"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Date</FormLabel>
|
<FormLabel>Date</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="Released on 24 Nov 2024" />
|
<Input {...field} placeholder="Released on 24 Nov 2024" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<FormField
|
<FormField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`bullets.${index}.point`}
|
name={`bullets.${index}.point`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Key Point {index + 1}</FormLabel>
|
<FormLabel>Key Point {index + 1}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
size={"icon"}
|
size={"icon"}
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
onClick={() => append({ point: "" })}
|
onClick={() => append({ point: "" })}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full text-lg rounded-full"
|
className="w-full text-lg rounded-full"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{/* Section to list and delete logs */}
|
{/* Section to list and delete logs */}
|
||||||
<section id="logs-list" className="py-16 md:py-24">
|
<section id="logs-list" className="py-16 md:py-24">
|
||||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Logs</h2>
|
<h2 className="text-3xl md:text-4xl font-bold">Existing Logs</h2>
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
<Table className="w-full mt-4 border-muted">
|
<Table className="w-full mt-4 border-muted">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||||
<TableHead className="border-b px-4 py-2">Date</TableHead>
|
<TableHead className="border-b px-4 py-2">Date</TableHead>
|
||||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{logs
|
{logs
|
||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((log) => (
|
.map((log) => (
|
||||||
<TableRow key={log._id}>
|
<TableRow key={log._id}>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
{log.version}
|
{log.version}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
{log.date}
|
{log.date}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
<Button
|
<Button
|
||||||
variant={"destructive"}
|
variant={"destructive"}
|
||||||
onClick={() => deleteLog(log._id)}
|
onClick={() => deleteLog(log._id)}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminLogPage;
|
export default AdminLogPage;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Admin // Downloads",
|
title: "Admin // Downloads"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,246 +6,246 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { UploadButton } from "@/lib/uploadthing";
|
import { UploadButton } from "@/lib/uploadthing";
|
||||||
import { downloadSchema } from "@/lib/validations/validation";
|
import { downloadSchema } from "@/lib/validations/validation";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
interface DownloadEntry {
|
interface DownloadEntry {
|
||||||
_id: string;
|
_id: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
version: string;
|
version: string;
|
||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
fileSize: string;
|
fileSize: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadsPage = () => {
|
const DownloadsPage = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [downloads, setDownloads] = useState<DownloadEntry[]>([]);
|
const [downloads, setDownloads] = useState<DownloadEntry[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof downloadSchema>>({
|
const form = useForm<z.infer<typeof downloadSchema>>({
|
||||||
resolver: zodResolver(downloadSchema),
|
resolver: zodResolver(downloadSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fileName: "",
|
fileName: "",
|
||||||
version: "",
|
version: "",
|
||||||
downloadLink: "",
|
downloadLink: "",
|
||||||
fileSize: "",
|
fileSize: ""
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchDownloads = async () => {
|
const fetchDownloads = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/downloads", {
|
const response = await fetch("/api/downloads", {
|
||||||
method: "GET",
|
method: "GET"
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data: DownloadEntry[] = await response.json();
|
const data: DownloadEntry[] = await response.json();
|
||||||
setDownloads(data);
|
setDownloads(data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message || "Failed to fetch downloads");
|
setError(error.message || "Failed to fetch downloads");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDownloads();
|
fetchDownloads();
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchDownloads();
|
fetchDownloads();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof downloadSchema>> = async (
|
const onSubmit: SubmitHandler<z.infer<typeof downloadSchema>> = async (
|
||||||
data
|
data
|
||||||
) => {
|
) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch("/api/upload", {
|
const response = await fetch("/api/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
form.reset();
|
form.reset();
|
||||||
fetchDownloads();
|
fetchDownloads();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast({ description: "Download Successfully Updated" });
|
toast({ description: "Download Successfully Updated" });
|
||||||
} else {
|
} else {
|
||||||
console.error("Upload failed");
|
console.error("Upload failed");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast({ description: "Uploading Failed", variant: "destructive" });
|
toast({ description: "Uploading Failed", variant: "destructive" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteDownload = async (id: string) => {
|
const deleteDownload = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/delete/downloads/${id}`, {
|
const response = await fetch(`/api/delete/downloads/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE"
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
fetchDownloads();
|
fetchDownloads();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message || "Failed to delete download");
|
setError(error.message || "Failed to delete download");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="downloads-page" className="wrapper container">
|
<section id="downloads-page" className="wrapper container">
|
||||||
<h1 className="text-3xl font-bold py-6">Downloads Form</h1>
|
<h1 className="text-3xl font-bold py-6">Downloads Form</h1>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="fileName"
|
name="fileName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>File Name</FormLabel>
|
<FormLabel>File Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="version"
|
name="version"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Version</FormLabel>
|
<FormLabel>Version</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="downloadLink"
|
name="downloadLink"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Download Link</FormLabel>
|
<FormLabel>Download Link</FormLabel>
|
||||||
<UploadButton
|
<UploadButton
|
||||||
endpoint="imageUploader"
|
endpoint="imageUploader"
|
||||||
onClientUploadComplete={(res) => {
|
onClientUploadComplete={(res) => {
|
||||||
field.onChange(res[0].url);
|
field.onChange(res[0].url);
|
||||||
}}
|
}}
|
||||||
onUploadError={(error: Error) => {
|
onUploadError={(error: Error) => {
|
||||||
alert(`ERROR! ${error.message}`);
|
alert(`ERROR! ${error.message}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="fileSize"
|
name="fileSize"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>File Size</FormLabel>
|
<FormLabel>File Size</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full text-lg rounded-full"
|
className="w-full text-lg rounded-full"
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{/* Section to list and delete downloads */}
|
{/* Section to list and delete downloads */}
|
||||||
<section id="downloads-list" className="py-16 md:py-24">
|
<section id="downloads-list" className="py-16 md:py-24">
|
||||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Downloads</h2>
|
<h2 className="text-3xl md:text-4xl font-bold">Existing Downloads</h2>
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
<Table className="w-full mt-4 border-muted">
|
<Table className="w-full mt-4 border-muted">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
||||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||||
<TableHead className="border-b px-4 py-2">
|
<TableHead className="border-b px-4 py-2">
|
||||||
Download Link
|
Download Link
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
||||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{downloads
|
{downloads
|
||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((download) => (
|
.map((download) => (
|
||||||
<TableRow key={download._id}>
|
<TableRow key={download._id}>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
{download.fileName}
|
{download.fileName}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
{download.version}
|
{download.version}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
<a
|
<a
|
||||||
href={download.downloadLink}
|
href={download.downloadLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{download.downloadLink}
|
{download.downloadLink}
|
||||||
</a>
|
</a>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
{download.fileSize}
|
{download.fileSize}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
<Button
|
<Button
|
||||||
variant={"destructive"}
|
variant={"destructive"}
|
||||||
onClick={() => deleteDownload(download._id)}
|
onClick={() => deleteDownload(download._id)}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DownloadsPage;
|
export default DownloadsPage;
|
||||||
|
|
133
app/(auth)/admin/email/page.tsx
Normal file
133
app/(auth)/admin/email/page.tsx
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
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
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious
|
||||||
|
} from "@/components/ui/pagination";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface Subscriber {
|
||||||
|
email: string;
|
||||||
|
subscribedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmailPage = () => {
|
||||||
|
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}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch data initially
|
||||||
|
fetchSubscribers();
|
||||||
|
|
||||||
|
// Set up interval to fetch data every 10 seconds
|
||||||
|
const intervalId = setInterval(fetchSubscribers, 10000);
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailPage;
|
16
app/(auth)/admin/layout.tsx
Normal file
16
app/(auth)/admin/layout.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import MobileNav from "../_components/Mobilenav";
|
||||||
|
import Sidebar from "../_components/Sidebar";
|
||||||
|
|
||||||
|
export default function PageLayout({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col min-h-screen root">
|
||||||
|
<Sidebar />
|
||||||
|
<MobileNav />
|
||||||
|
<div className="root-container lg:px-24">{children}</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Admin // Mods",
|
title: "Admin // Mods"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,249 +6,395 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { UploadButton } from "@/lib/uploadthing";
|
import { UploadButton } from "@/lib/uploadthing";
|
||||||
import { modsSchema } from "@/lib/validations/validation";
|
import { modsSchema } from "@/lib/validations/validation";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
interface ModEntry {
|
interface ModEntry {
|
||||||
_id: string;
|
_id: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
version: string;
|
version: string;
|
||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
fileSize: string;
|
fileSize: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SvrjsModsAdminPage = () => {
|
const SvrjsModsAdminPage = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [mods, setMods] = useState<ModEntry[]>([]);
|
const [mods, setMods] = useState<ModEntry[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [editMod, setEditMod] = useState<ModEntry | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof modsSchema>>({
|
const mainForm = useForm<z.infer<typeof modsSchema>>({
|
||||||
resolver: zodResolver(modsSchema),
|
resolver: zodResolver(modsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fileName: "",
|
fileName: "",
|
||||||
version: "",
|
version: "",
|
||||||
downloadLink: "",
|
downloadLink: "",
|
||||||
fileSize: "",
|
fileSize: ""
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchMods = async () => {
|
const dialogForm = useForm<z.infer<typeof modsSchema>>({
|
||||||
try {
|
resolver: zodResolver(modsSchema),
|
||||||
const response = await fetch("/api/mods", {
|
defaultValues: {
|
||||||
method: "GET",
|
fileName: "",
|
||||||
});
|
version: "",
|
||||||
if (response.ok) {
|
downloadLink: "",
|
||||||
const data: ModEntry[] = await response.json();
|
fileSize: ""
|
||||||
setMods(data);
|
}
|
||||||
} else {
|
});
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
setError(error.message || "Failed to fetch mods");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMods();
|
fetchMods();
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchMods();
|
fetchMods();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof modsSchema>> = async (data) => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
if (editMod) {
|
||||||
const response = await fetch("/api/uploadmods", {
|
dialogForm.reset({
|
||||||
method: "POST",
|
fileName: editMod.fileName,
|
||||||
headers: {
|
version: editMod.version,
|
||||||
"Content-Type": "application/json",
|
downloadLink: editMod.downloadLink,
|
||||||
},
|
fileSize: editMod.fileSize
|
||||||
body: JSON.stringify(data),
|
});
|
||||||
});
|
setDialogOpen(true); // Open dialog when a mod is being edited
|
||||||
|
}
|
||||||
|
}, [editMod, dialogForm]);
|
||||||
|
|
||||||
if (response.ok) {
|
const fetchMods = async () => {
|
||||||
form.reset();
|
try {
|
||||||
fetchMods();
|
const response = await fetch("/api/mods", {
|
||||||
setLoading(false);
|
method: "GET"
|
||||||
toast({
|
});
|
||||||
description: "Successfully Uploaded Mods",
|
if (response.ok) {
|
||||||
});
|
const data: ModEntry[] = await response.json();
|
||||||
} else {
|
setMods(data);
|
||||||
console.error("Upload failed");
|
} else {
|
||||||
setLoading(false);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
toast({
|
}
|
||||||
description: "Upload failed",
|
} catch (error: any) {
|
||||||
variant: "destructive",
|
setError(error.message || "Failed to fetch mods");
|
||||||
});
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const deleteMod = async (id: string) => {
|
const onSubmit: SubmitHandler<z.infer<typeof modsSchema>> = async (data) => {
|
||||||
try {
|
setLoading(true);
|
||||||
const response = await fetch(`/api/delete/mods/${id}`, {
|
try {
|
||||||
method: "DELETE",
|
const response = editMod
|
||||||
});
|
? await fetch(`/api/update/mods/${editMod._id}`, {
|
||||||
if (response.ok) {
|
method: "PUT",
|
||||||
fetchMods();
|
headers: {
|
||||||
} else {
|
"Content-Type": "application/json"
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
},
|
||||||
}
|
body: JSON.stringify(data)
|
||||||
} catch (error: any) {
|
})
|
||||||
setError(error.message || "Failed to delete mod");
|
: await fetch("/api/uploadmods", {
|
||||||
}
|
method: "POST",
|
||||||
};
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
if (response.ok) {
|
||||||
<section id="mods-page" className="wrapper container">
|
mainForm.reset();
|
||||||
<h1 className="text-3xl font-bold py-6">Mods Form</h1>
|
dialogForm.reset();
|
||||||
<Form {...form}>
|
fetchMods();
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
setLoading(false);
|
||||||
<FormField
|
setEditMod(null);
|
||||||
control={form.control}
|
setDialogOpen(false); // Close dialog on successful submission
|
||||||
name="fileName"
|
toast({
|
||||||
render={({ field }) => (
|
description: "Successfully Saved Changes"
|
||||||
<FormItem>
|
});
|
||||||
<FormLabel>File Name</FormLabel>
|
} else {
|
||||||
<FormControl>
|
console.error("Save failed");
|
||||||
<Input {...field} />
|
setLoading(false);
|
||||||
</FormControl>
|
toast({
|
||||||
<FormMessage />
|
description: "Save failed",
|
||||||
</FormItem>
|
variant: "destructive"
|
||||||
)}
|
});
|
||||||
/>
|
}
|
||||||
<FormField
|
} catch (error) {
|
||||||
control={form.control}
|
console.error("Save failed", error);
|
||||||
name="version"
|
setLoading(false);
|
||||||
render={({ field }) => (
|
toast({
|
||||||
<FormItem>
|
description: "Save failed",
|
||||||
<FormLabel>Version</FormLabel>
|
variant: "destructive"
|
||||||
<FormControl>
|
});
|
||||||
<Input {...field} />
|
}
|
||||||
</FormControl>
|
};
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="downloadLink"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Download Link</FormLabel>
|
|
||||||
<UploadButton
|
|
||||||
endpoint="imageUploader"
|
|
||||||
onClientUploadComplete={(res) => {
|
|
||||||
field.onChange(res[0].url);
|
|
||||||
}}
|
|
||||||
onUploadError={(error: Error) => {
|
|
||||||
alert(`ERROR! ${error.message}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="fileSize"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>File Size</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full text-lg rounded-full"
|
|
||||||
size={"lg"}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{/* Section to list and delete mods */}
|
const deleteMod = async (id: string) => {
|
||||||
<section id="mods-list" className="py-16 md:py-24">
|
try {
|
||||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Mods</h2>
|
const response = await fetch(`/api/delete/mods/${id}`, {
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
method: "DELETE"
|
||||||
<Table className="w-full mt-4 border-muted">
|
});
|
||||||
<TableHeader>
|
if (response.ok) {
|
||||||
<TableRow>
|
fetchMods();
|
||||||
<TableHead className="border-b px-4 py-2">File Name</TableHead>
|
} else {
|
||||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
<TableHead className="border-b px-4 py-2">
|
}
|
||||||
Download Link
|
} catch (error: any) {
|
||||||
</TableHead>
|
setError(error.message || "Failed to delete mod");
|
||||||
<TableHead className="border-b px-4 py-2">File Size</TableHead>
|
}
|
||||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
};
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
return (
|
||||||
<TableBody>
|
<section id="mods-page" className="wrapper container">
|
||||||
{mods
|
<h1 className="text-3xl font-bold py-6">Mods Form</h1>
|
||||||
.slice()
|
<Form {...mainForm}>
|
||||||
.reverse()
|
<form onSubmit={mainForm.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
.map((mod) => (
|
<FormField
|
||||||
<TableRow key={mod._id}>
|
control={mainForm.control}
|
||||||
<TableCell className="border-b px-4 py-2">
|
name="fileName"
|
||||||
{mod.fileName}
|
render={({ field }) => (
|
||||||
</TableCell>
|
<FormItem>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<FormLabel>File Name</FormLabel>
|
||||||
{mod.version}
|
<FormControl>
|
||||||
</TableCell>
|
<Input {...field} />
|
||||||
<TableCell className="border-b px-4 py-2">
|
</FormControl>
|
||||||
<a
|
<FormMessage />
|
||||||
href={mod.downloadLink}
|
</FormItem>
|
||||||
target="_blank"
|
)}
|
||||||
rel="noopener noreferrer"
|
/>
|
||||||
>
|
<FormField
|
||||||
{mod.downloadLink}
|
control={mainForm.control}
|
||||||
</a>
|
name="version"
|
||||||
</TableCell>
|
render={({ field }) => (
|
||||||
<TableCell className="border-b px-4 py-2">
|
<FormItem>
|
||||||
{mod.fileSize}
|
<FormLabel>Version</FormLabel>
|
||||||
</TableCell>
|
<FormControl>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<Input {...field} />
|
||||||
<Button
|
</FormControl>
|
||||||
variant={"destructive"}
|
<FormMessage />
|
||||||
onClick={() => deleteMod(mod._id)}
|
</FormItem>
|
||||||
>
|
)}
|
||||||
Delete
|
/>
|
||||||
</Button>
|
<FormField
|
||||||
</TableCell>
|
control={mainForm.control}
|
||||||
</TableRow>
|
name="downloadLink"
|
||||||
))}
|
render={({ field }) => (
|
||||||
</TableBody>
|
<FormItem>
|
||||||
</Table>
|
<FormLabel>Download Link</FormLabel>
|
||||||
</section>
|
<UploadButton
|
||||||
</section>
|
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.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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SvrjsModsAdminPage;
|
export default SvrjsModsAdminPage;
|
||||||
|
|
|
@ -7,73 +7,79 @@ import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), {
|
const MarkdownEditor = dynamic(() => import("@uiw/react-md-editor"), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const EditPage = ({ params }: { params: { slug: string } }) => {
|
const EditPage = ({ params }: { params: { slug: string } }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { slug } = params;
|
const { slug } = params;
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [vulnerabilities, setVulnerabilities] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
fetch(`/api/mdx/pages/${slug}`)
|
fetch(`/api/mdx/pages/${slug}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setTitle(data.title);
|
setTitle(data.title);
|
||||||
setContent(data.content);
|
setContent(data.content);
|
||||||
})
|
setVulnerabilities(data.vulnerabilities || "");
|
||||||
.catch((error) => console.error("Failed to load page", error));
|
})
|
||||||
}
|
.catch((error) => console.error("Failed to load page", error));
|
||||||
}, [slug]);
|
}
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
const savePage = async () => {
|
const savePage = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ title, content }),
|
body: JSON.stringify({ title, content, vulnerabilities })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast({ description: "Page successfully updated" });
|
toast({ description: "Page successfully updated" });
|
||||||
router.push(`/admin/multi-logs/`);
|
router.push(`/admin/multi-logs/`);
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
// TEMPERARORY ERROR
|
toast({ description: "Page Updated" });
|
||||||
router.push(`/admin/multi-logs/`);
|
}
|
||||||
toast({ description: "Updated but cant return data" });
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditorChange = (value?: string) => {
|
const handleEditorChange = (value?: string) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
setContent(value);
|
setContent(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="edit-page" className="wrapper container">
|
<section id="edit-page" className="wrapper container gap-4">
|
||||||
<h1 className="text-3xl font-bold py-6">Edit Page: {slug}</h1>
|
<h1 className="text-3xl font-bold py-6">Edit Page: {slug}</h1>
|
||||||
<Input
|
<Input
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="Page Title"
|
placeholder="Page Title"
|
||||||
/>
|
/>
|
||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
value={content}
|
value={content}
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
height={560}
|
height={560}
|
||||||
/>
|
/>
|
||||||
<Button onClick={savePage} disabled={loading} className="mt-4">
|
<h1 className="text-3xl font-bold py-6">Vulnerabilities</h1>
|
||||||
Save
|
<MarkdownEditor
|
||||||
</Button>
|
value={vulnerabilities}
|
||||||
</section>
|
onChange={(value) => setVulnerabilities(value || "")}
|
||||||
);
|
height={200}
|
||||||
|
/>
|
||||||
|
<Button onClick={savePage} disabled={loading} className="mt-4">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditPage;
|
export default EditPage;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Admin // MultiLogs",
|
title: "Admin // MultiLogs"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,157 +2,157 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
interface PageEntry {
|
interface PageEntry {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultiLogs = () => {
|
const MultiLogs = () => {
|
||||||
const [pages, setPages] = useState<PageEntry[]>([]);
|
const [pages, setPages] = useState<PageEntry[]>([]);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [pageTitle, setPageTitle] = useState("");
|
const [pageTitle, setPageTitle] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/mdx/pages")
|
fetch("/api/mdx/pages")
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => setPages(data))
|
.then((data) => setPages(data))
|
||||||
.catch((error) => console.error("Failed to load pages", error));
|
.catch((error) => console.error("Failed to load pages", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const createPage = async () => {
|
const createPage = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const slug = pageTitle.toLowerCase().replace(/\s+/g, "-");
|
const slug = pageTitle.toLowerCase().replace(/\s+/g, "-");
|
||||||
const response = await fetch("/api/mdx/pages", {
|
const response = await fetch("/api/mdx/pages", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ title: pageTitle, slug, content: "" }),
|
body: JSON.stringify({ title: pageTitle, slug, content: "" })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const newPage = await response.json();
|
const newPage = await response.json();
|
||||||
setPages([...pages, newPage]);
|
setPages([...pages, newPage]);
|
||||||
setPageTitle("");
|
setPageTitle("");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
router.push(`/admin/multi-logs/${slug}`);
|
router.push(`/admin/multi-logs/${slug}`);
|
||||||
toast({ description: "Page created successfully" });
|
toast({ description: "Page created successfully" });
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
console.error("Failed to create page:", errorData);
|
console.error("Failed to create page:", errorData);
|
||||||
toast({ description: `Error: ${errorData.message}` });
|
toast({ description: `Error: ${errorData.message}` });
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePage = async (slug: string) => {
|
const deletePage = async (slug: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
const response = await fetch(`/api/mdx/pages/${slug}`, {
|
||||||
method: "DELETE",
|
method: "DELETE"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setPages(pages.filter((page) => page.slug !== slug));
|
setPages(pages.filter((page) => page.slug !== slug));
|
||||||
toast({ description: "Page deleted successfully" });
|
toast({ description: "Page deleted successfully" });
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
console.error("Failed to delete page:", errorData);
|
console.error("Failed to delete page:", errorData);
|
||||||
toast({ description: `Error: ${errorData.message}` });
|
toast({ description: `Error: ${errorData.message}` });
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="logs-page" className="wrapper container">
|
<section id="logs-page" className="wrapper container">
|
||||||
<section id="create-page" className="py-16">
|
<section id="create-page" className="py-16">
|
||||||
<h2 className="text-3xl md:text-4xl font-bold mb-2">Create New Page</h2>
|
<h2 className="text-3xl md:text-4xl font-bold mb-2">Create New Page</h2>
|
||||||
<Button variant={"secondary"} onClick={() => setOpen(true)}>
|
<Button variant={"secondary"} onClick={() => setOpen(true)}>
|
||||||
Create New Page
|
Create New Page
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Enter Page Title</DialogTitle>
|
<DialogTitle>Enter Page Title</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Input
|
<Input
|
||||||
value={pageTitle}
|
value={pageTitle}
|
||||||
onChange={(e) => setPageTitle(e.target.value)}
|
onChange={(e) => setPageTitle(e.target.value)}
|
||||||
placeholder="Page Title"
|
placeholder="Page Title"
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button disabled={loading} onClick={createPage}>
|
<Button disabled={loading} onClick={createPage}>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</section>
|
</section>
|
||||||
<section id="pages-list" className="pb-16">
|
<section id="pages-list" className="pb-16">
|
||||||
<h2 className="text-3xl md:text-4xl font-bold">Existing Pages</h2>
|
<h2 className="text-3xl md:text-4xl font-bold">Existing Pages</h2>
|
||||||
<p className="mb-4">Total Pages: {pages.length}</p>
|
<p className="mb-4">Total Pages: {pages.length}</p>
|
||||||
<Table className="w-full mt-4 border-muted">
|
<Table className="w-full mt-4 border-muted">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="border-b px-4 py-2">Slug</TableHead>
|
<TableHead className="border-b px-4 py-2">Slug</TableHead>
|
||||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<TableRow key={page.slug}>
|
<TableRow key={page.slug}>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
<a
|
<a
|
||||||
href={`/changelogs/${page.slug}`}
|
href={`/changelog/${page.slug}`}
|
||||||
className="text-blue-500 underline"
|
className="text-blue-500 underline"
|
||||||
>
|
>
|
||||||
{page.slug}
|
{page.slug}
|
||||||
</a>
|
</a>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
<Button
|
<Button
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(`/admin/multi-logs/${page.slug}`)
|
router.push(`/admin/multi-logs/${page.slug}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={"destructive"}
|
variant={"destructive"}
|
||||||
onClick={() => deletePage(page.slug)}
|
onClick={() => deletePage(page.slug)}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MultiLogs;
|
export default MultiLogs;
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Card from "../_components/Card";
|
import Card from "../_components/Card";
|
||||||
|
import { AdminDashboardLINKS } from "@/constants";
|
||||||
|
|
||||||
const AdminPage = () => {
|
const AdminPage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section id="adminpage" className="wrapper container">
|
<section id="adminpage" className="wrapper container">
|
||||||
<h1 className="h2-bold py-6">Admin Page</h1>
|
<h1 className="h2-bold py-6">Admin Page</h1>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 grid-cols-1 gap-4 ">
|
<div className="grid lg:grid-cols-2 grid-cols-1 gap-4 ">
|
||||||
<Card title="Downloads" url="/admin/downloads" />
|
{AdminDashboardLINKS.map((item, idx) => (
|
||||||
<Card title="Mods" url="/admin/mods" />
|
<Card key={idx} title={item.label} url={item.url} />
|
||||||
<Card title="Logs" url="/admin/changelogs" />
|
))}
|
||||||
<Card title="MultiLogs" url="/admin/multi-logs" />
|
</div>
|
||||||
<Card title="Vulnerabilities" url="/admin/vulnerabilities" />
|
</section>
|
||||||
</div>
|
</>
|
||||||
</section>
|
);
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminPage;
|
export default AdminPage;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Admin // Vulnerabilities",
|
title: "Admin // Vulnerabilities"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function logPages({ children }: { children: React.ReactNode }) {
|
export default function logPages({ children }: { children: React.ReactNode }) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,213 +5,238 @@ import { useForm, SubmitHandler, useFieldArray } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { logsSchema } from "@/lib/validations/validation";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { vulnerabilitiesSchema } from "@/lib/validations/validation";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
interface LogEntry {
|
interface VulnerabiltyEntry {
|
||||||
_id: string;
|
_id: string;
|
||||||
version: string;
|
version: string;
|
||||||
date: string;
|
bullets: {
|
||||||
bullets: { point: string }[];
|
point: string;
|
||||||
|
securityAdvisoryUrl: string;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogsFormValues = z.infer<typeof logsSchema>;
|
type VulnerabiltiesForm = z.infer<typeof vulnerabilitiesSchema>;
|
||||||
|
|
||||||
const AdminLogPage = () => {
|
const AdminLogPage = () => {
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<VulnerabiltyEntry[]>([]);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const form = useForm<LogsFormValues>({
|
const form = useForm<VulnerabiltiesForm>({
|
||||||
resolver: zodResolver(logsSchema),
|
resolver: zodResolver(vulnerabilitiesSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
version: "",
|
version: "",
|
||||||
date: "",
|
bullets: [{ point: "", securityAdvisoryUrl: "" }]
|
||||||
bullets: [{ point: "" }],
|
}
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: "bullets",
|
name: "bullets"
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/vulnerabilties", { method: "GET" });
|
const response = await fetch("/api/vulnerabilities", { method: "GET" });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data: LogEntry[] = await response.json();
|
const data: VulnerabiltyEntry[] = await response.json();
|
||||||
setLogs(data);
|
setLogs(data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message || "Failed to fetch logs");
|
setError(error.message || "Failed to fetch logs");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
|
const onSubmit: SubmitHandler<VulnerabiltiesForm> = async (data) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch("/api/uploadvulnerabilities", {
|
const response = await fetch("/api/uploadvulnerabilities", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
form.reset();
|
form.reset();
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast({ description: "Logs successfully added" });
|
toast({ description: "Logs successfully added" });
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast({ description: "Upload Failed", variant: "destructive" });
|
toast({ description: "Upload Failed", variant: "destructive" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteLog = async (id: string) => {
|
const deleteLog = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/delete/logs/${id}`, {
|
const response = await fetch(`/api/delete/vulnerability/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE"
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error.message || "Failed to delete log");
|
setError(error.message || "Failed to delete log");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="logs-page" className="wrapper container">
|
<section id="logs-page" className="wrapper container">
|
||||||
<h1 className="text-3xl font-bold py-6">Server Vulnerabilties Form</h1>
|
<h1 className="text-3xl font-bold py-6">Server Vulnerabilties Form</h1>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="version"
|
name="version"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Version Name</FormLabel>
|
<FormLabel>Version Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<FormField
|
<>
|
||||||
key={field.id}
|
<FormField
|
||||||
control={form.control}
|
key={field.id}
|
||||||
name={`bullets.${index}.point`}
|
control={form.control}
|
||||||
render={({ field }) => (
|
name={`bullets.${index}.point`}
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Key Point {index + 1}</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Key Point {index + 1}</FormLabel>
|
||||||
<Input {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<Input {...field} />
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
<Button
|
<FormMessage />
|
||||||
type="button"
|
</FormItem>
|
||||||
className="mt-2"
|
)}
|
||||||
variant={"secondary"}
|
/>
|
||||||
onClick={() => remove(index)}
|
<FormField
|
||||||
>
|
key={field.id + "-securityAdvisory"}
|
||||||
Remove
|
control={form.control}
|
||||||
</Button>
|
name={`bullets.${index}.securityAdvisoryUrl`}
|
||||||
</FormItem>
|
render={({ field }) => (
|
||||||
)}
|
<FormItem>
|
||||||
/>
|
<FormLabel>
|
||||||
))}
|
Security Advisory URL for Key Point {index + 1}
|
||||||
<Button
|
</FormLabel>
|
||||||
type="button"
|
<FormControl>
|
||||||
className="mb-4"
|
<Input {...field} />
|
||||||
size={"icon"}
|
</FormControl>
|
||||||
variant={"outline"}
|
<FormMessage />
|
||||||
onClick={() => append({ point: "" })}
|
<Button
|
||||||
>
|
type="button"
|
||||||
+
|
className="mt-2"
|
||||||
</Button>
|
variant={"secondary"}
|
||||||
<Button
|
onClick={() => remove(index)}
|
||||||
type="submit"
|
>
|
||||||
className="w-full text-lg rounded-full"
|
Remove
|
||||||
disabled={loading}
|
</Button>
|
||||||
size={"lg"}
|
</FormItem>
|
||||||
>
|
)}
|
||||||
Submit
|
/>
|
||||||
</Button>
|
</>
|
||||||
</form>
|
))}
|
||||||
</Form>
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mb-4"
|
||||||
|
size={"icon"}
|
||||||
|
variant={"outline"}
|
||||||
|
onClick={() => append({ point: "", securityAdvisoryUrl: "" })}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</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 to list and delete logs */}
|
||||||
<section id="logs-list" className="py-16 md:py-24">
|
<section id="logs-list" className="py-16 md:py-24">
|
||||||
<h2 className="text-3xl md:text-4xl font-bold">
|
<h2 className="text-3xl md:text-4xl font-bold">
|
||||||
Existing Vulnerabilties
|
Existing Vulnerabilties
|
||||||
</h2>
|
</h2>
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
<Table className="w-full mt-4 border-muted">
|
<Table className="w-full mt-4 border-muted">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
<TableHead className="border-b px-4 py-2">Version</TableHead>
|
||||||
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
<TableHead className="border-b px-4 py-2">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{logs
|
{logs
|
||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((log) => (
|
.map((log) => (
|
||||||
<TableRow key={log._id}>
|
<TableRow key={log._id}>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
{log.version}
|
{log.version}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="border-b px-4 py-2">
|
<TableCell className="border-b px-4 py-2">
|
||||||
<Button
|
<Button
|
||||||
variant={"destructive"}
|
variant={"destructive"}
|
||||||
onClick={() => deleteLog(log._id)}
|
onClick={() => deleteLog(log._id)}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminLogPage;
|
export default AdminLogPage;
|
||||||
|
|
149
app/(auth)/email-editor/page.tsx
Normal file
149
app/(auth)/email-editor/page.tsx
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import CodeEditor from "@/components/cards/MonacoEditor";
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
“{"{unsubscribeId}"}” will be replaced with an
|
||||||
|
unsubscription ID.
|
||||||
|
<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>
|
||||||
|
<iframe
|
||||||
|
className="border rounded-md w-full h-2/3"
|
||||||
|
srcDoc={previewContent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailEditor;
|
|
@ -1,16 +1,10 @@
|
||||||
import MobileNav from "./_components/Mobilenav";
|
import React from "react";
|
||||||
import Sidebar from "./_components/Sidebar";
|
import AuthProvider from "@/components/shared/providers/AuthProvider";
|
||||||
|
|
||||||
export default function PageLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return <AuthProvider>{children}</AuthProvider>;
|
||||||
<main className="flex flex-col min-h-screen root">
|
|
||||||
<Sidebar />
|
|
||||||
<MobileNav />
|
|
||||||
<div className="root-container lg:px-24">{children}</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
123
app/(root)/blog/[slug]/_styles/prism-twilight.css
Normal file
123
app/(root)/blog/[slug]/_styles/prism-twilight.css
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* okaidia theme for JavaScript, CSS and HTML
|
||||||
|
* Loosely based on Monokai textmate theme by http://www.monokai.nl/
|
||||||
|
* @author ocodia
|
||||||
|
*/
|
||||||
|
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #f8f8f2;
|
||||||
|
background: none;
|
||||||
|
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #272822;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: 0.1em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #8292a2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.namespace {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #f92672;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.boolean,
|
||||||
|
.token.number {
|
||||||
|
color: #ae81ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #a6e22e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string,
|
||||||
|
.token.variable {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.function,
|
||||||
|
.token.class-name {
|
||||||
|
color: #e6db74;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.keyword {
|
||||||
|
color: #66d9ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
color: #fd971f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
98
app/(root)/blog/[slug]/_styles/prism.twilight.min.css
vendored
Normal file
98
app/(root)/blog/[slug]/_styles/prism.twilight.min.css
vendored
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #f8f8f2;
|
||||||
|
background: 0 0;
|
||||||
|
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
}
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #272822;
|
||||||
|
}
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: 0.1em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.token.cdata,
|
||||||
|
.token.comment,
|
||||||
|
.token.doctype,
|
||||||
|
.token.prolog {
|
||||||
|
color: #8292a2;
|
||||||
|
}
|
||||||
|
.token.punctuation {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
.token.namespace {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.token.constant,
|
||||||
|
.token.deleted,
|
||||||
|
.token.property,
|
||||||
|
.token.symbol,
|
||||||
|
.token.tag {
|
||||||
|
color: #f92672;
|
||||||
|
}
|
||||||
|
.token.boolean,
|
||||||
|
.token.number {
|
||||||
|
color: #ae81ff;
|
||||||
|
}
|
||||||
|
.token.attr-name,
|
||||||
|
.token.builtin,
|
||||||
|
.token.char,
|
||||||
|
.token.inserted,
|
||||||
|
.token.selector,
|
||||||
|
.token.string {
|
||||||
|
color: #a6e22e;
|
||||||
|
}
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string,
|
||||||
|
.token.entity,
|
||||||
|
.token.operator,
|
||||||
|
.token.url,
|
||||||
|
.token.variable {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.class-name,
|
||||||
|
.token.function {
|
||||||
|
color: #e6db74;
|
||||||
|
}
|
||||||
|
.token.keyword {
|
||||||
|
color: #66d9ef;
|
||||||
|
}
|
||||||
|
.token.important,
|
||||||
|
.token.regex {
|
||||||
|
color: #fd971f;
|
||||||
|
}
|
||||||
|
.token.bold,
|
||||||
|
.token.important {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
231
app/(root)/blog/[slug]/page.tsx
Normal file
231
app/(root)/blog/[slug]/page.tsx
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
import { client, urlFor } from "@/lib/sanity";
|
||||||
|
import { PortableText, PortableTextComponents } from "@portabletext/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowLeft, Rss } from "lucide-react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import Prism from "prismjs";
|
||||||
|
import "prismjs/components/prism-javascript";
|
||||||
|
import "prismjs/components/prism-python";
|
||||||
|
import "prismjs/components/prism-php";
|
||||||
|
import "prismjs/components/prism-bash";
|
||||||
|
import "prismjs/components/prism-sql";
|
||||||
|
import "prismjs/components/prism-yaml";
|
||||||
|
import "prismjs/components/prism-markdown";
|
||||||
|
import "prismjs/components/prism-json";
|
||||||
|
import "prismjs/components/prism-perl";
|
||||||
|
|
||||||
|
import CopyButton from "@/components/shared/copyButton";
|
||||||
|
import "./_styles/prism-twilight.css";
|
||||||
|
import "./_styles/prism.twilight.min.css";
|
||||||
|
import PrismLoader from "@/components/loader/prismLoader";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
async function getData(slug: string) {
|
||||||
|
const query = `
|
||||||
|
*[_type == "blog" && slug.current == '${slug}'] {
|
||||||
|
"currentSlug": slug.current,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
smallDescription,
|
||||||
|
titleImage,
|
||||||
|
_createdAt
|
||||||
|
}[0]`;
|
||||||
|
|
||||||
|
const data = await client.fetch(query, {}, { cache: "no-store" });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogSlugArticle {
|
||||||
|
currentSlug: string;
|
||||||
|
title: string;
|
||||||
|
content: any;
|
||||||
|
titleImage: string;
|
||||||
|
_createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params
|
||||||
|
}: {
|
||||||
|
params: { slug: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const data = await getData(params.slug);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
title: "404 Not Found - SVR.JS",
|
||||||
|
openGraph: {
|
||||||
|
title: "404 Not Found - SVR.JS"
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
title: "404 Not Found - SVR.JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${data.title} - SVR.JS`,
|
||||||
|
description: data.smallDescription,
|
||||||
|
openGraph: {
|
||||||
|
title: `${data.title} - SVR.JS`,
|
||||||
|
description: data.smallDescription,
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog/${data.currentSlug}`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: data.titleImage
|
||||||
|
? urlFor(data.titleImage).url()
|
||||||
|
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog-missing.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: `${data.title} - SVR.JS`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: `${data.title} - SVR.JS`,
|
||||||
|
description: data.smallDescription,
|
||||||
|
images: [
|
||||||
|
data.titleImage
|
||||||
|
? urlFor(data.titleImage).url()
|
||||||
|
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog-missing.png`
|
||||||
|
],
|
||||||
|
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 || "none";
|
||||||
|
const grammar = Prism.languages[language];
|
||||||
|
|
||||||
|
if (language != "none" && !grammar) {
|
||||||
|
console.error(`No grammar found for language: "${language}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
{language == "none" ? "" : <PrismLoader />}
|
||||||
|
<CopyButton code={value.code} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function BlogSlugArticle({
|
||||||
|
params
|
||||||
|
}: {
|
||||||
|
params: { slug: string };
|
||||||
|
}) {
|
||||||
|
const data: BlogSlugArticle = await getData(params.slug);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
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-primary"
|
||||||
|
>
|
||||||
|
<Rss className="w-5 h-5 mr-1" /> Subscribe to RSS
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<header className="text-start mb-8 w-full">
|
||||||
|
<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={
|
||||||
|
data.titleImage
|
||||||
|
? urlFor(data.titleImage).url()
|
||||||
|
: "/blog-missing.png"
|
||||||
|
}
|
||||||
|
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">
|
||||||
|
Published on: {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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const query = `*[_type == 'blog']{
|
||||||
|
"slug": slug.current,
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const slugsRaw = await client.fetch(query);
|
||||||
|
|
||||||
|
return slugsRaw;
|
||||||
|
}
|
|
@ -1,11 +1,63 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
import BlogCards from "@/components/cards/BlogCards";
|
||||||
|
import { Rss } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Blog - SVRJS",
|
title: "Blog - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Blog - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Blog - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const BlogPage = () => {
|
|
||||||
return <div>BlogPage</div>;
|
const BlogPage = async () => {
|
||||||
|
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 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 Blog
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground flex-center mb-2">
|
||||||
|
Our blog has web development, web server administration, and web
|
||||||
|
application security tips.
|
||||||
|
<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 page={1} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BlogPage;
|
export default BlogPage;
|
||||||
|
|
89
app/(root)/blog/page/[id]/page.tsx
Normal file
89
app/(root)/blog/page/[id]/page.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React from "react";
|
||||||
|
import { client } from "@/lib/sanity";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import BlogCards from "@/components/cards/BlogCards";
|
||||||
|
import { Rss } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Blog - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Blog - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Blog - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlogPage = async ({ params }: { params: { id: string } }) => {
|
||||||
|
// Optionally, you can fetch some initial data here if needed.
|
||||||
|
let id = parseInt(params.id);
|
||||||
|
if (isNaN(id)) id = 1;
|
||||||
|
|
||||||
|
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 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 Blog
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground flex-center mb-2">
|
||||||
|
Our blog has web development, web server administration, and web
|
||||||
|
application security tips.
|
||||||
|
<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 page={id} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
// Change in BlogCards component and in /api/revalidate route too!
|
||||||
|
const cardsPerPage = 6;
|
||||||
|
|
||||||
|
const totalPostsQuery = `count(*[_type == 'blog'])`;
|
||||||
|
const totalPosts: number = await client.fetch(
|
||||||
|
totalPostsQuery,
|
||||||
|
{},
|
||||||
|
{ cache: "no-store" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalPosts / cardsPerPage);
|
||||||
|
|
||||||
|
let ids: any[] = [];
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
ids.push({ id: i.toString() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogPage;
|
80
app/(root)/changelog/[slug]/layout.tsx
Normal file
80
app/(root)/changelog/[slug]/layout.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
interface Page {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
|
export async function generateMetadata({
|
||||||
|
params
|
||||||
|
}: {
|
||||||
|
params: { slug: "string" };
|
||||||
|
}) {
|
||||||
|
let page: Page = {
|
||||||
|
title: "unknown mod",
|
||||||
|
content: "unknown mod"
|
||||||
|
};
|
||||||
|
let notFound = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
|
const fetchedPage = (await db
|
||||||
|
.collection("pages")
|
||||||
|
.findOne({ slug: params.slug })) as unknown as Page;
|
||||||
|
if (fetchedPage) {
|
||||||
|
page = fetchedPage;
|
||||||
|
} else {
|
||||||
|
notFound = true;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
if (notFound) {
|
||||||
|
return {
|
||||||
|
title: "404 Not Found - SVR.JS",
|
||||||
|
openGraph: {
|
||||||
|
title: "404 Not Found - SVR.JS"
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
title: "404 Not Found - SVR.JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${page.title} change log - SVR.JS`,
|
||||||
|
description: `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.`,
|
||||||
|
openGraph: {
|
||||||
|
title: `${page.title} change log - SVR.JS`,
|
||||||
|
description: `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.`,
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/changelog/${params.slug}`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: `${page.title} change log - SVR.JS`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: `${page.title} change log - SVR.JS`,
|
||||||
|
description: `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.`,
|
||||||
|
images: [
|
||||||
|
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`
|
||||||
|
],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const ContactLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactLayout;
|
69
app/(root)/changelog/[slug]/page.tsx
Normal file
69
app/(root)/changelog/[slug]/page.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import React from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import Head from "next/head";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
interface Page {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
const Page = async ({ params }: { params: { slug: string } }) => {
|
||||||
|
const { slug } = params;
|
||||||
|
let page: Page | null = null;
|
||||||
|
let isNotFound = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
|
const fetchedPage = (await db
|
||||||
|
.collection("pages")
|
||||||
|
.findOne({ slug })) as unknown as Page;
|
||||||
|
if (fetchedPage) {
|
||||||
|
page = fetchedPage;
|
||||||
|
} else {
|
||||||
|
isNotFound = true;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
if (isNotFound) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="wrapper container py-24 md:py-28 gap-2 flex flex-col">
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
{page.title} change log
|
||||||
|
</h1>
|
||||||
|
<ReactMarkdown className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
|
{page.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
const slugs = await db.collection("pages").find().toArray();
|
||||||
|
return slugs.map((element) => {
|
||||||
|
return { slug: element.slug };
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page;
|
37
app/(root)/changelog/layout.tsx
Normal file
37
app/(root)/changelog/layout.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "SVR.JS change log - SVR.JS",
|
||||||
|
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: "SVR.JS change log - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/changelog`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "SVR.JS change log - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "SVR.JS change log - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ContactLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <main>{children}</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactLayout;
|
69
app/(root)/changelog/page.tsx
Normal file
69
app/(root)/changelog/page.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
interface Bullet {
|
||||||
|
point: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LOGS {
|
||||||
|
_id: string;
|
||||||
|
date: string;
|
||||||
|
version: string;
|
||||||
|
bullets?: Bullet[]; // Make bullets optional
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
const LogsPage: React.FC = async () => {
|
||||||
|
let error: Error | null = null;
|
||||||
|
let downloads: LOGS[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
downloads = (await db
|
||||||
|
.collection("logs")
|
||||||
|
.find()
|
||||||
|
.toArray()) as unknown as LOGS[];
|
||||||
|
} catch (err) {
|
||||||
|
error = err as Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedDownloads = [...downloads].reverse();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="logs"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
|
SVR.JS change log
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
See the changes done to SVR.JS web server.
|
||||||
|
</p>
|
||||||
|
{error && <p className="text-red-500">{error.message}</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>{download.version}</h2>
|
||||||
|
<span className="italic">{download.date}</span>
|
||||||
|
<ul>
|
||||||
|
{(download.bullets ?? []).map((bullet, index) => (
|
||||||
|
<li key={index}>{bullet.point}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsPage;
|
|
@ -1,87 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import Head from "next/head";
|
|
||||||
|
|
||||||
const Page = ({ params }: { params: { slug: string } }) => {
|
|
||||||
const { slug } = params;
|
|
||||||
const [page, setPage] = useState<{ title: string; content: string } | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [notFound, setNotFound] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchPage = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/mdx/pages/${slug}`);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setPage(data);
|
|
||||||
return (document.title = `${data.title} | SVRJS`);
|
|
||||||
} else {
|
|
||||||
if (response.status === 404) {
|
|
||||||
setNotFound(true);
|
|
||||||
return (document.title = "404 Not Found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load page", error);
|
|
||||||
setNotFound(true);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchPage();
|
|
||||||
}, [slug]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
|
||||||
<div className="mb-3">
|
|
||||||
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
|
||||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
|
||||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
|
||||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notFound) {
|
|
||||||
return (
|
|
||||||
<section id="404error" className="flex-center flex-col wrapper container">
|
|
||||||
<h1 className="text-3xl md:text-5xl text-center">
|
|
||||||
<span className="text-red-500">404</span> Page not Found
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg mt-3 text-muted-foreground">
|
|
||||||
Please return back to Home
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section className="wrapper container py-24 md:py-28 gap-2 flex flex-col">
|
|
||||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
|
||||||
{page.title}
|
|
||||||
</h1>
|
|
||||||
<ReactMarkdown className="prose max-w-full md:prose-lg dark:prose-invert">
|
|
||||||
{page.content}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
|
@ -1,86 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Download } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import { CHANGE_LOGS } from "@/constants/guidelines";
|
|
||||||
|
|
||||||
interface Bullet {
|
|
||||||
point: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LOGS {
|
|
||||||
_id: string;
|
|
||||||
date: string;
|
|
||||||
version: string;
|
|
||||||
bullets?: Bullet[]; // Make bullets optional
|
|
||||||
}
|
|
||||||
|
|
||||||
const LogsPage: React.FC = () => {
|
|
||||||
const [downloads, setDownloads] = useState<LOGS[]>([]);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const fetchDownloads = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/logs", {
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data: LOGS[] = await response.json();
|
|
||||||
setDownloads(data);
|
|
||||||
} else {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
setError(error.message || "Failed to fetch downloads");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDownloads();
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchDownloads();
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
const reversedDownloads = [...downloads].reverse();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
id="logs"
|
|
||||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
|
||||||
>
|
|
||||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
|
||||||
Server LOGS
|
|
||||||
</h1>
|
|
||||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
|
||||||
Get all the latest version of SVRJS download and compiled Files here!
|
|
||||||
</p>
|
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
|
||||||
|
|
||||||
{reversedDownloads.map((download) => (
|
|
||||||
<div
|
|
||||||
key={download._id}
|
|
||||||
className="flex-start prose max-w-full md:prose-lg dark:prose-invert flex-col mb-4"
|
|
||||||
>
|
|
||||||
<h2 className="font-bold text-3xl">{download.version}</h2>
|
|
||||||
<span className="font-medium italic">{download.date}</span>
|
|
||||||
<ul className="list-disc pl-5">
|
|
||||||
{(download.bullets ?? []).map((bullet, index) => (
|
|
||||||
<li key={index}>{bullet.point}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
|
||||||
<ReactMarkdown>{CHANGE_LOGS}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogsPage;
|
|
37
app/(root)/contact/layout.tsx
Normal file
37
app/(root)/contact/layout.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Contact Us - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/contact`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Contact Us - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Contact Us - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ContactLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <main>{children}</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactLayout;
|
|
@ -7,12 +7,12 @@ import { z } from "zod";
|
||||||
import { contactFormSchema } from "@/lib/validations/validation";
|
import { contactFormSchema } from "@/lib/validations/validation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
@ -20,160 +20,182 @@ import { useToast } from "@/components/ui/use-toast";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { emails } from "@/constants";
|
import { emails } from "@/constants";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
|
|
||||||
const ContactUs = () => {
|
const ContactUs = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||||
|
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof contactFormSchema>>({
|
const form = useForm<z.infer<typeof contactFormSchema>>({
|
||||||
resolver: zodResolver(contactFormSchema),
|
resolver: zodResolver(contactFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
message: "",
|
message: ""
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
|
||||||
setLoading(true);
|
if (!captchaToken) {
|
||||||
try {
|
setShowCaptcha(true);
|
||||||
const res = await fetch("/api/contact", {
|
return;
|
||||||
method: "POST",
|
}
|
||||||
body: JSON.stringify(values),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
setLoading(true);
|
||||||
form.reset();
|
try {
|
||||||
toast({
|
const res = await fetch("/api/contact", {
|
||||||
description: "Your message has been sent.",
|
method: "POST",
|
||||||
});
|
body: JSON.stringify({ ...values, captchaToken }),
|
||||||
setLoading(false);
|
headers: {
|
||||||
} else {
|
"Content-Type": "application/json",
|
||||||
toast({
|
Accept: "application/json"
|
||||||
title: "Uh oh! Something went wrong.",
|
}
|
||||||
variant: "destructive",
|
});
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast({
|
|
||||||
title: "Uh oh! Something went wrong.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
if (res.ok) {
|
||||||
<>
|
form.reset();
|
||||||
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300">
|
setCaptchaToken(null); // Reset captcha token after successful submission
|
||||||
<h1 className="text-4xl md:text-6xl tracking-tight font-bold uppercase text-center text-gray-900 dark:text-white">
|
toast({
|
||||||
Contact Us
|
description: "Your message has been sent."
|
||||||
</h1>
|
});
|
||||||
</div>
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<section id="contact" className="w-full">
|
function handleCaptchaVerify(token: string) {
|
||||||
<div className="flex-center flex-col md:flex-row justify-between mx-auto p-6 max-w-5xl">
|
setCaptchaToken(token);
|
||||||
{/* Left contact page */}
|
onSubmit(form.getValues()); // Trigger form submission after captcha is verified
|
||||||
<Form {...form}>
|
}
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="space-y-4 pb-8 mb-8 max-w-lg w-full bg-accent border p-6 rounded-lg shadow-md"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Your Name" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="email" placeholder="Your Email" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="message"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Message</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
className="h-44"
|
|
||||||
placeholder="Your Message"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant={"default"}
|
|
||||||
className="w-full mt-2"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<span className="tracking-tight font-semibold">SEND</span>
|
|
||||||
<Send className="ml-2 w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{/* Right contact page */}
|
return (
|
||||||
<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">
|
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300">
|
||||||
{emails.map((email, index) => (
|
<h1 className="text-4xl md:text-6xl tracking-tight font-bold uppercase text-center text-gray-900 dark:text-white">
|
||||||
<li
|
Contact Us
|
||||||
key={index}
|
</h1>
|
||||||
className="text-gray-600 dark:text-gray-300 flex items-center"
|
</div>
|
||||||
>
|
|
||||||
<email.icon className="mr-2" size={24} />
|
<section id="contact" className="w-full">
|
||||||
<span>
|
<div className="flex-center flex-col md:flex-row justify-between mx-auto p-6 max-w-5xl">
|
||||||
<a
|
{/* Left contact page */}
|
||||||
href={email.url}
|
<Form {...form}>
|
||||||
title={`Send an email to ${email.email}`}
|
<form
|
||||||
className="text-muted-foreground hover:text-accent-foreground transition duration-200"
|
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"
|
||||||
{email.email}
|
>
|
||||||
</a>
|
<FormField
|
||||||
</span>
|
control={form.control}
|
||||||
</li>
|
name="name"
|
||||||
))}
|
render={({ field }) => (
|
||||||
</ul>
|
<FormItem>
|
||||||
<Separator />
|
<FormLabel>Name</FormLabel>
|
||||||
<ul className="flex justify-center space-x-3 my-6">
|
<FormControl>
|
||||||
<Iconss />
|
<Input placeholder="Your Name" {...field} />
|
||||||
</ul>
|
</FormControl>
|
||||||
<Separator />
|
<FormMessage />
|
||||||
<div className="text-center text-gray-500 mt-2 text-sm font-light"></div>
|
</FormItem>
|
||||||
</div>
|
)}
|
||||||
</div>
|
/>
|
||||||
</section>
|
<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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant={"default"}
|
||||||
|
className="w-full mt-2"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<span className="tracking-tight font-semibold">SEND</span>
|
||||||
|
<Send className="ml-2 w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* Right contact page */}
|
||||||
|
<div className="max-w-lg mt-8 md:mt-0 md:ml-8 p-12 border rounded-lg">
|
||||||
|
<ul className="space-y-4 mb-6">
|
||||||
|
{emails.map((email, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="text-gray-600 dark:text-gray-300 flex items-center"
|
||||||
|
>
|
||||||
|
<email.icon className="mr-2" size={24} />
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
href={email.url}
|
||||||
|
title={`Send an email to ${email.email}`}
|
||||||
|
className="text-muted-foreground hover:text-accent-foreground transition duration-200"
|
||||||
|
>
|
||||||
|
{email.email}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Separator />
|
||||||
|
<ul className="flex justify-center space-x-3 my-6">
|
||||||
|
<Iconss />
|
||||||
|
</ul>
|
||||||
|
<Separator />
|
||||||
|
<div className="text-center text-gray-500 mt-2 text-sm font-light"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContactUs;
|
export default ContactUs;
|
||||||
|
|
|
@ -2,28 +2,55 @@ import ReactMarkdown from "react-markdown";
|
||||||
import { contribute } from "@/constants/guidelines";
|
import { contribute } from "@/constants/guidelines";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Contribute - SVRJS",
|
title: "Contribute - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/contribute`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Contribute - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Contribute - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Contribute = () => {
|
const Contribute = () => {
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="contribute"
|
id="tos"
|
||||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
>
|
>
|
||||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
Contributing to SVR.JS
|
Contributing to SVR.JS
|
||||||
</h1>
|
</h1>
|
||||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
We welcome contributions from the community! Here's how you can
|
We welcome contributions from the community! Here's how you can
|
||||||
help!
|
help!
|
||||||
</p>
|
</p>
|
||||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
<ReactMarkdown>{contribute}</ReactMarkdown>
|
<ReactMarkdown>{contribute}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Contribute;
|
export default Contribute;
|
||||||
|
|
|
@ -1,13 +1,40 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Downloads - SVRJS",
|
title: "Downloads - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/downloads`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Downloads - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Downloads - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DownloadLayout({
|
export default function DownloadLayout({
|
||||||
children,
|
children
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return <main>{children}</main>;
|
return <main>{children}</main>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,106 +1,105 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Download } from "lucide-react";
|
import { Download } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
interface Download {
|
interface Download {
|
||||||
_id: string;
|
_id: string;
|
||||||
date: string;
|
date: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
version: string;
|
version: string;
|
||||||
fileSize: string;
|
fileSize: string;
|
||||||
downloadLink: string;
|
downloadLink?: string; // Optional
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadPage: React.FC = () => {
|
export const dynamic = "force-static";
|
||||||
const [downloads, setDownloads] = useState<Download[]>([]);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const fetchDownloads = async () => {
|
const DownloadPage: React.FC = async () => {
|
||||||
try {
|
let error: Error | null = null;
|
||||||
const response = await fetch("/api/downloads", {
|
let downloads: Download[] = [];
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data: Download[] = await response.json();
|
|
||||||
setDownloads(data);
|
|
||||||
} else {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
setError(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
try {
|
||||||
fetchDownloads();
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
downloads = (await db
|
||||||
|
.collection("downloads")
|
||||||
|
.find()
|
||||||
|
.toArray()) as unknown as Download[];
|
||||||
|
} catch (err) {
|
||||||
|
error = err as Error;
|
||||||
|
}
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
return (
|
||||||
fetchDownloads();
|
<section
|
||||||
}, 10000);
|
id="download"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
return () => clearInterval(interval);
|
>
|
||||||
}, []);
|
<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
|
||||||
return (
|
</h1>
|
||||||
<section
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
id="download"
|
Get all the latest versions of SVR.JS here! Other SVR.JS downloads can
|
||||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
be found in{" "}
|
||||||
>
|
<Link
|
||||||
<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">
|
href="https://downloads.svrjs.org"
|
||||||
Downloads
|
className="text-black dark:text-white underline"
|
||||||
</h1>
|
>
|
||||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
SVR.JS downloads server
|
||||||
Get all the latest version of SVRJS download and compiled Files here!
|
</Link>
|
||||||
</p>
|
.
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
</p>
|
||||||
<Table>
|
{error && <p className="text-red-500">{error.message}</p>}
|
||||||
<TableCaption>A list of all available downloads.</TableCaption>
|
<Table>
|
||||||
<TableHeader>
|
<TableCaption>A list of all available downloads.</TableCaption>
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead className="w-[150px]">Date</TableHead>
|
<TableRow>
|
||||||
<TableHead>File Name</TableHead>
|
<TableHead className="w-[150px]">Date</TableHead>
|
||||||
<TableHead>Version</TableHead>
|
<TableHead>File Name</TableHead>
|
||||||
<TableHead>File Size</TableHead>
|
<TableHead>Version</TableHead>
|
||||||
<TableHead className="text-right">Download Link</TableHead>
|
<TableHead>File Size</TableHead>
|
||||||
</TableRow>
|
<TableHead className="text-right">Download Link</TableHead>
|
||||||
</TableHeader>
|
</TableRow>
|
||||||
<TableBody>
|
</TableHeader>
|
||||||
{downloads
|
<TableBody>
|
||||||
.slice(0, 10)
|
{downloads
|
||||||
.reverse()
|
.slice(downloads.length - 10 < 0 ? 0 : downloads.length - 10)
|
||||||
.map((download) => (
|
.reverse()
|
||||||
<TableRow key={download._id}>
|
.map((download) => (
|
||||||
<TableCell className="font-medium">{download.date}</TableCell>
|
<TableRow key={download._id}>
|
||||||
<TableCell>{download.fileName}</TableCell>
|
<TableCell className="font-medium">{download.date}</TableCell>
|
||||||
<TableCell>{download.version}</TableCell>
|
<TableCell>{download.fileName}</TableCell>
|
||||||
<TableCell className="text-left">{download.fileSize}</TableCell>
|
<TableCell>{download.version}</TableCell>
|
||||||
<TableCell className="flex items-center justify-end">
|
<TableCell className="text-left">{download.fileSize}</TableCell>
|
||||||
<Link href={download.downloadLink}>
|
<TableCell className="flex items-center justify-end">
|
||||||
<Button variant={"ghost"} className="">
|
{download.downloadLink ? (
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<Link href={download.downloadLink}>
|
||||||
Download
|
<Button variant={"ghost"} className="">
|
||||||
</Button>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
</Link>
|
Download
|
||||||
</TableCell>
|
</Button>
|
||||||
</TableRow>
|
</Link>
|
||||||
))}
|
) : (
|
||||||
</TableBody>
|
<Button variant={"ghost"} disabled>
|
||||||
</Table>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
</section>
|
Unavailable
|
||||||
);
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DownloadPage;
|
export default DownloadPage;
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Forum - SVRJS",
|
|
||||||
};
|
|
||||||
|
|
||||||
const Forum = () => {
|
|
||||||
return <div>Forum</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Forum;
|
|
|
@ -1,47 +1,61 @@
|
||||||
import Footer from "@/components/shared/Footer";
|
import Footer from "@/components/shared/Footer";
|
||||||
import Navbar from "@/components/shared/Navbar";
|
import Navbar from "@/components/shared/Navbar";
|
||||||
|
import Banner from "@/components/widgets/Banner";
|
||||||
|
import NoScript from "@/components/shared/NoScript";
|
||||||
|
import { Rocket } from "lucide-react";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
// baseURL [ENV]
|
// baseURL [ENV]
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "SVRJS - A Web Server running on Node.js",
|
title: "SVR.JS - a web server running on Node.JS",
|
||||||
description:
|
description:
|
||||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.JS. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "SVRJS - A Web Server running on Node.js",
|
title: "SVR.JS - a web server running on Node.JS",
|
||||||
description:
|
description:
|
||||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.JS. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||||
url: "https://svrjs.org",
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}`,
|
||||||
type: "website",
|
type: "website",
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
alt: "SVRJS - A Web Server running on Node.js",
|
alt: "SVR.JS - a web server running on Node.JS"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
site: "@SVR_JS",
|
site: "@SVR_JS",
|
||||||
title: "SVRJS - A Web Server running on Node.js",
|
title: "SVR.JS - a web server running on Node.JS",
|
||||||
description:
|
description:
|
||||||
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.JS. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
|
||||||
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
|
images: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
creator: "@SVR_JS",
|
creator: "@SVR_JS"
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PageLayout({
|
export default function PageLayout({
|
||||||
children,
|
children
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
const iconClassName = "w-4 h-4 flex-center text-zinc-950 -mr-2";
|
||||||
<div className="flex flex-col min-h-screen">
|
return (
|
||||||
<Navbar />
|
<div className="flex flex-col min-h-screen">
|
||||||
<div className="flex-grow flex-1 overflow-x-hidden">{children}</div>
|
{/* Comment or edit this whenever required */}
|
||||||
<Footer />
|
{/*<Banner
|
||||||
</div>
|
icon={<Rocket className={iconClassName} />}
|
||||||
);
|
title="SVR.JS 4.0.0 has been released!"
|
||||||
|
announcement="This major release brings many improvements to SVR.JS."
|
||||||
|
link="/blog/svr-js-4-0-0-has-been-released"
|
||||||
|
buttonText="Read more"
|
||||||
|
/>*/}
|
||||||
|
<Navbar />
|
||||||
|
<NoScript />
|
||||||
|
<div className="flex-grow flex-1 overflow-x-hidden">{children}</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,35 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "MOD - SVRJS",
|
title: "Mods - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/mods`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Mods - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Mods - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModLayout = ({ children }: { children: React.ReactNode }) => {
|
const ModLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
return <main>{children}</main>;
|
return <main>{children}</main>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,106 +1,101 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Download } from "lucide-react";
|
import { Download } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
interface Mods {
|
interface Mods {
|
||||||
_id: string;
|
_id: string;
|
||||||
date: string;
|
date: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
version: string;
|
version: string;
|
||||||
fileSize: string;
|
fileSize: string;
|
||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModsPage: React.FC = () => {
|
export const dynamic = "force-static";
|
||||||
const [downloads, setDownloads] = useState<Mods[]>([]);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const fetchDownloads = async () => {
|
const ModsPage: React.FC = async () => {
|
||||||
try {
|
let error: Error | null = null;
|
||||||
const response = await fetch("/api/mods", {
|
let downloads: Mods[] = [];
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data: Mods[] = await response.json();
|
|
||||||
setDownloads(data);
|
|
||||||
} else {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
setError(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
try {
|
||||||
fetchDownloads();
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
downloads = (await db
|
||||||
|
.collection("mods")
|
||||||
|
.find()
|
||||||
|
.toArray()) as unknown as Mods[];
|
||||||
|
} catch (err) {
|
||||||
|
error = err as Error;
|
||||||
|
}
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
return (
|
||||||
fetchDownloads();
|
<section
|
||||||
}, 10000);
|
id="mods"
|
||||||
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
return () => clearInterval(interval);
|
>
|
||||||
}, []);
|
<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 mods
|
||||||
return (
|
</h1>
|
||||||
<section
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
id="mods"
|
Get all the latest version of SVR.JS mods here! Notes can be found at{" "}
|
||||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
<Link
|
||||||
>
|
href="/docs/mod-notes"
|
||||||
<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">
|
className="text-black dark:text-white underline"
|
||||||
SvrJS Mods
|
>
|
||||||
</h1>
|
“SVR.JS mod notes” section in SVR.JS documentation
|
||||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
</Link>
|
||||||
Get all the latest version of SVRJS Mods and compiled Files here!{" "}
|
. Other SVR.JS mods downloads can be found in{" "}
|
||||||
</p>
|
<Link
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
href="https://downloads.svrjs.org/mods"
|
||||||
<Table>
|
className="text-black dark:text-white underline"
|
||||||
<TableCaption>A list of all available downloads.</TableCaption>
|
>
|
||||||
<TableHeader>
|
SVR.JS downloads server
|
||||||
<TableRow>
|
</Link>
|
||||||
<TableHead className="w-[150px]">Date</TableHead>
|
.
|
||||||
<TableHead>File Name</TableHead>
|
</p>
|
||||||
<TableHead>Version</TableHead>
|
{error && <p className="text-red-500">{error.message}</p>}
|
||||||
<TableHead>Download Link</TableHead>
|
<Table>
|
||||||
<TableHead className="text-right">File Size</TableHead>
|
<TableCaption>A list of all available downloads.</TableCaption>
|
||||||
</TableRow>
|
<TableHeader>
|
||||||
</TableHeader>
|
<TableRow>
|
||||||
<TableBody>
|
<TableHead className="w-[150px]">Date</TableHead>
|
||||||
{downloads
|
<TableHead>File Name</TableHead>
|
||||||
.slice(0, 10)
|
<TableHead>Version</TableHead>
|
||||||
.reverse()
|
<TableHead>File Size</TableHead>
|
||||||
.map((download) => (
|
<TableHead className="text-right">Download Link</TableHead>
|
||||||
<TableRow key={download._id}>
|
</TableRow>
|
||||||
<TableCell className="font-medium">{download.date}</TableCell>
|
</TableHeader>
|
||||||
<TableCell>{download.fileName}</TableCell>
|
<TableBody>
|
||||||
<TableCell>{download.version}</TableCell>
|
{downloads.map((download) => (
|
||||||
<TableCell className="text-left">{download.fileSize}</TableCell>
|
<TableRow key={download._id}>
|
||||||
<TableCell className="flex items-center justify-end">
|
<TableCell className="font-medium">{download.date}</TableCell>
|
||||||
<Link href={download.downloadLink}>
|
<TableCell>{download.fileName}</TableCell>
|
||||||
<Button variant={"ghost"} className="">
|
<TableCell>{download.version}</TableCell>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<TableCell className="text-left">{download.fileSize}</TableCell>
|
||||||
Download
|
<TableCell className="flex items-center justify-end">
|
||||||
</Button>
|
<Link href={download.downloadLink}>
|
||||||
</Link>
|
<Button variant={"ghost"} className="">
|
||||||
</TableCell>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
</TableRow>
|
Download
|
||||||
))}
|
</Button>
|
||||||
</TableBody>
|
</Link>
|
||||||
</Table>
|
</TableCell>
|
||||||
</section>
|
</TableRow>
|
||||||
);
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModsPage;
|
export default ModsPage;
|
||||||
|
|
39
app/(root)/newsletter/page.tsx
Normal file
39
app/(root)/newsletter/page.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import Newsletter from "@/components/shared/Newsletter";
|
||||||
|
import React from "react";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Newsletter - SVR.JS",
|
||||||
|
description:
|
||||||
|
"Subscribe to our newsletter for updates. We promise no spam emails will be sent.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Newsletter - SVR.JS",
|
||||||
|
description:
|
||||||
|
"Subscribe to our newsletter for updates. We promise no spam emails will be sent.",
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/newsletter`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Newsletter - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Newsletter - SVR.JS",
|
||||||
|
description:
|
||||||
|
"Subscribe to our newsletter for updates. We promise no spam emails will be sent.",
|
||||||
|
images: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewsletterPage = () => {
|
||||||
|
return <Newsletter />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsletterPage;
|
|
@ -2,24 +2,23 @@ import About from "@/components/shared/About";
|
||||||
import DataTable from "@/components/shared/DataTable";
|
import DataTable from "@/components/shared/DataTable";
|
||||||
import Faq from "@/components/shared/FAQ";
|
import Faq from "@/components/shared/FAQ";
|
||||||
import Hero from "@/components/shared/Hero";
|
import Hero from "@/components/shared/Hero";
|
||||||
import HowItWorks from "@/components/shared/HowItWorks";
|
import Features from "@/components/shared/Features";
|
||||||
import Newsletter from "@/components/shared/Newsletter";
|
import Newsletter from "@/components/shared/Newsletter";
|
||||||
import Partners from "@/components/shared/Partners";
|
import DemoVideo from "@/components/shared/DemoVideo";
|
||||||
import Testimonials from "@/components/shared/Testimonials";
|
import Testimonials from "@/components/shared/Testimonials";
|
||||||
|
|
||||||
const RootPage = () => {
|
const RootPage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero />
|
<Hero />
|
||||||
<HowItWorks />
|
<Features />
|
||||||
<Testimonials />
|
<Testimonials />
|
||||||
<Partners />
|
<DemoVideo />
|
||||||
<About />
|
<About />
|
||||||
{/* <DataTable /> */}
|
<Faq />
|
||||||
<Faq />
|
<Newsletter />
|
||||||
<Newsletter />
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RootPage;
|
export default RootPage;
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { PRIVACY_POLICY } from "@/constants/guidelines";
|
|
||||||
import React from "react";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
|
|
||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Privacy Policy - SVRJS",
|
|
||||||
};
|
|
||||||
|
|
||||||
const PrivacyPolicy = () => {
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
id="privacy-policy"
|
|
||||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
|
||||||
>
|
|
||||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
|
||||||
Privacy Policy
|
|
||||||
</h1>
|
|
||||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
|
||||||
Effective date: 26.05.2024
|
|
||||||
</p>
|
|
||||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
|
||||||
<ReactMarkdown>{PRIVACY_POLICY}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PrivacyPolicy;
|
|
57
app/(root)/privacy/page.tsx
Normal file
57
app/(root)/privacy/page.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { PRIVACY_POLICY } from "@/constants/guidelines";
|
||||||
|
import React from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Privacy Policy - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/privacy`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Privacy Policy - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Privacy Policy - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/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: September 7, 2024
|
||||||
|
</p>
|
||||||
|
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
|
<ReactMarkdown>{PRIVACY_POLICY}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivacyPolicy;
|
|
@ -2,27 +2,54 @@ import ReactMarkdown from "react-markdown";
|
||||||
import { TERMS_AND_CONDITIONS } from "@/constants/guidelines";
|
import { TERMS_AND_CONDITIONS } from "@/constants/guidelines";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Terms Of Service - SVRJS",
|
title: "Terms of Service - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/tos`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Terms of Service - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Terms of Service - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TermsOfService = () => {
|
const TermsOfService = () => {
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="tos"
|
id="tos"
|
||||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
>
|
>
|
||||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
Terms and Conditions
|
Terms of Service
|
||||||
</h1>
|
</h1>
|
||||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
Last updated: 24.04.2024
|
Effective date: April 24, 2024
|
||||||
</p>
|
</p>
|
||||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
||||||
<ReactMarkdown>{TERMS_AND_CONDITIONS}</ReactMarkdown>
|
<ReactMarkdown>{TERMS_AND_CONDITIONS}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TermsOfService;
|
export default TermsOfService;
|
||||||
|
|
36
app/(root)/unsubscribe/layout.tsx
Normal file
36
app/(root)/unsubscribe/layout.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React from "react";
|
||||||
|
export async function generateMetadata() {
|
||||||
|
return {
|
||||||
|
title: "Unsubscribe - SVR.JS",
|
||||||
|
description: "Unsubscribe from our newsletter.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Unsubscribe - SVR.JS",
|
||||||
|
description: "Unsubscribe from our newsletter.",
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/unsubscribe?id=`, // We can't use searchParams in layouts
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Unsubscribe - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Unsubscribe - SVR.JS",
|
||||||
|
description: "Unsubscribe from our newsletter.",
|
||||||
|
images: [
|
||||||
|
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`
|
||||||
|
],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const UnsubscribeLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <main>{children}</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnsubscribeLayout;
|
96
app/(root)/unsubscribe/page.tsx
Normal file
96
app/(root)/unsubscribe/page.tsx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
"use client";
|
||||||
|
import Newsletter from "@/components/shared/Newsletter";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
|
|
||||||
|
const UnsubscribePage = ({
|
||||||
|
searchParams
|
||||||
|
}: {
|
||||||
|
searchParams: { id: string | undefined };
|
||||||
|
}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showCaptcha, setShowCaptcha] = useState(false);
|
||||||
|
|
||||||
|
const handleCaptchaVerify = async (token: string) => {
|
||||||
|
setShowCaptcha(false);
|
||||||
|
await submit(token); // Trigger form submission after captcha is verified
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async (captchaToken: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/unsubscribe", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ unsubscribeId: searchParams.id, captchaToken }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
toast({
|
||||||
|
description: "Unsubscribed successfully."
|
||||||
|
});
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
Unsubscribe from newsletter
|
||||||
|
</h1>
|
||||||
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
|
Are you sure to unsubscribe from the newsletter? You will no longer
|
||||||
|
receive updates from the newsletter.
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
className="mx-auto text-center"
|
||||||
|
onSubmit={(e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowCaptcha(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant={"default"}
|
||||||
|
className="mt-2"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<span className="tracking-tight font-semibold">Unsubscribe</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
{showCaptcha && (
|
||||||
|
<HCaptcha
|
||||||
|
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||||
|
onVerify={handleCaptchaVerify}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnsubscribePage;
|
37
app/(root)/vulnerabilities/layout.tsx
Normal file
37
app/(root)/vulnerabilities/layout.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
// baseURL [ENV]
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Vulnerabilities - SVR.JS",
|
||||||
|
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 - SVR.JS",
|
||||||
|
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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/vulnerabilities`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "Vulnerabilities - SVR.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "Vulnerabilities - SVR.JS",
|
||||||
|
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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ModLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <main>{children}</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModLayout;
|
|
@ -1,108 +1,127 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { vulnerabilities } from "@/constants/guidelines";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface Bullet {
|
interface Bullet {
|
||||||
point: string;
|
point: string;
|
||||||
|
securityAdvisoryUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Vulnerabilities {
|
interface Vulnerabilities {
|
||||||
_id: string;
|
_id: string;
|
||||||
date: string;
|
version: string;
|
||||||
version: string;
|
bullets?: Bullet[]; // Make bullets optional
|
||||||
bullets?: Bullet[]; // Make bullets optional
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Vulnerabilities = () => {
|
interface ModsVulnerability {
|
||||||
const [loading, setLoading] = useState(true);
|
_id: string;
|
||||||
const [downloads, setDownloads] = useState<Vulnerabilities[]>([]);
|
title: string;
|
||||||
const [error, setError] = useState("");
|
slug: string;
|
||||||
|
content: string;
|
||||||
|
vulnerabilities: string;
|
||||||
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const Vulnerabilities = async () => {
|
||||||
try {
|
let downloads: Vulnerabilities[] = [];
|
||||||
const response = await fetch("/api/vulnerabilities", {
|
let mods: ModsVulnerability[] = [];
|
||||||
method: "GET",
|
let error: Error | null = null;
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data: Vulnerabilities[] = await response.json();
|
|
||||||
setDownloads(data);
|
|
||||||
return (document.title = "Vulnerabilities | SVRJS");
|
|
||||||
} else {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
setError(error.message || "Failed to fetch downloads");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
try {
|
||||||
fetchData();
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
downloads = (await db
|
||||||
|
.collection("vulnerabilities")
|
||||||
|
.find()
|
||||||
|
.toArray()) as unknown as Vulnerabilities[];
|
||||||
|
} catch (err) {
|
||||||
|
error = err as Error;
|
||||||
|
}
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
try {
|
||||||
fetchData();
|
const client = await clientPromise;
|
||||||
}, 10000);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
const pages = (await db
|
||||||
}, []);
|
.collection("pages")
|
||||||
const reversedDownloads = [...downloads].reverse();
|
.find()
|
||||||
|
.toArray()) as unknown as ModsVulnerability[];
|
||||||
|
mods = pages.filter(
|
||||||
|
(mod: ModsVulnerability) =>
|
||||||
|
mod.vulnerabilities && mod.vulnerabilities.trim() !== ""
|
||||||
|
);
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
// initially loading = true
|
const reversedDownloads = [...downloads].reverse();
|
||||||
if (loading) {
|
const reversedMods = [...mods].reverse();
|
||||||
return (
|
|
||||||
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
|
|
||||||
<div className="mb-3">
|
|
||||||
<Skeleton className="w-[400px] h-[50px] rounded-md" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Skeleton className="w-[300px] h-[30px] rounded-md" />
|
|
||||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
|
||||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
|
||||||
<Skeleton className="w-[200px] h-[20px] rounded-md" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="vulnerabilities"
|
id="vulnerabilities"
|
||||||
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
|
||||||
>
|
>
|
||||||
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
|
||||||
SVR.JS Vulnerabilities
|
SVR.JS vulnerabilities
|
||||||
</h1>
|
</h1>
|
||||||
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
<p className="md:text-lg text-muted-foreground text-start mb-6">
|
||||||
Some older versions of SVR.JS are vulnerable to cyberattacks. It's
|
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
|
recommended to update your SVR.JS version to the newest one. If you find
|
||||||
a security issue with SVR.JS, report it as soon as possible to
|
a security issue with SVR.JS, report it as soon as possible to
|
||||||
vulnerability-reports[at]svrjs[dot]org. We'll mitigate that
|
vulnerability-reports@svrjs.org. We'll mitigate that vulnerability
|
||||||
vulnerability if it is possible.
|
if it is possible.
|
||||||
</p>
|
</p>
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
{error && <p className="text-red-500">{error.message}</p>}
|
||||||
|
|
||||||
{reversedDownloads.map((download) => (
|
<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">
|
||||||
<div
|
SVR.JS
|
||||||
key={download._id}
|
</h2>
|
||||||
className="flex-start flex-col prose dark:prose-invert mb-4 gap-4"
|
{reversedDownloads.map((download) => (
|
||||||
>
|
<div
|
||||||
<h2 className="font-semibold text-3xl -mb-2">{download.version}</h2>
|
key={download._id}
|
||||||
<ul className="list-disc pl-5">
|
className="flex-start flex-col prose max-w-full md:prose-lg dark:prose-invert gap-2"
|
||||||
{(download.bullets ?? []).map((bullet, index) => (
|
>
|
||||||
<li key={index}>{bullet.point}</li>
|
<h3 className="mb-0 md:mb-0">{download.version}</h3>
|
||||||
))}
|
<ul>
|
||||||
</ul>
|
{(download.bullets ?? []).map((bullet, index) => (
|
||||||
</div>
|
<li key={index}>
|
||||||
))}
|
{bullet.point}
|
||||||
<div className="prose max-w-full md:prose-lg dark:prose-invert">
|
{bullet.securityAdvisoryUrl ? (
|
||||||
<ReactMarkdown>{vulnerabilities}</ReactMarkdown>
|
<>
|
||||||
</div>
|
{" "}
|
||||||
</section>
|
<Link href={bullet.securityAdvisoryUrl} className="italic">
|
||||||
);
|
View the security advisory
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Section with MODS content */}
|
||||||
|
{reversedMods.map((mod) => (
|
||||||
|
<div
|
||||||
|
key={mod._id}
|
||||||
|
className="flex-start flex-col mt-6 md:mt-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 const dynamic = "force-static";
|
||||||
|
|
||||||
export default Vulnerabilities;
|
export default Vulnerabilities;
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const authOptions: NextAuthOptions = {
|
||||||
name: "Credentials",
|
name: "Credentials",
|
||||||
credentials: {
|
credentials: {
|
||||||
username: { label: "Username", type: "text" },
|
username: { label: "Username", type: "text" },
|
||||||
password: { label: "Password", type: "password" },
|
password: { label: "Password", type: "password" }
|
||||||
},
|
},
|
||||||
async authorize(credentials: any): Promise<any> {
|
async authorize(credentials: any): Promise<any> {
|
||||||
const adminUsername = process.env.ADMIN_USERNAME;
|
const adminUsername = process.env.ADMIN_USERNAME;
|
||||||
|
@ -18,7 +18,7 @@ export const authOptions: NextAuthOptions = {
|
||||||
console.log(adminUsername);
|
console.log(adminUsername);
|
||||||
console.log(adminPasswordHash);
|
console.log(adminPasswordHash);
|
||||||
console.log(credentials.username);
|
console.log(credentials.username);
|
||||||
console.log(credentials.password);
|
console.log("[password redacted]");
|
||||||
if (credentials.username == adminUsername) {
|
if (credentials.username == adminUsername) {
|
||||||
const isValidPassword = await bcrypt.compare(
|
const isValidPassword = await bcrypt.compare(
|
||||||
credentials.password,
|
credentials.password,
|
||||||
|
@ -34,8 +34,8 @@ export const authOptions: NextAuthOptions = {
|
||||||
}
|
}
|
||||||
// If you return null then an error will be displayed that the user to check their details.
|
// If you return null then an error will be displayed that the user to check their details.
|
||||||
return null;
|
return null;
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
|
@ -51,13 +51,13 @@ export const authOptions: NextAuthOptions = {
|
||||||
// session.user.id = token.id;
|
// session.user.id = token.id;
|
||||||
// session.user.name = token.name;
|
// session.user.name = token.name;
|
||||||
return session;
|
return session;
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
signIn: "/login",
|
signIn: "/login"
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
strategy: "jwt",
|
strategy: "jwt"
|
||||||
},
|
},
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.NEXTAUTH_SECRET
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,44 +1,39 @@
|
||||||
import { mailOptions, transporter } from "@/lib/nodemailer/nodemailer";
|
import { mailOptions, transporter } from "@/lib/nodemailer/nodemailer";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import dns from "dns/promises";
|
||||||
|
import { isEmail, escape } from "validator";
|
||||||
|
|
||||||
const CONTACT_MESSAGE_FIELDS: Record<string, string> = {
|
const CONTACT_MESSAGE_FIELDS: Record<string, string> = {
|
||||||
name: "Name",
|
name: "Name",
|
||||||
email: "Email",
|
email: "Email",
|
||||||
message: "Message",
|
message: "Message"
|
||||||
};
|
|
||||||
|
|
||||||
const escapeHtml = (text: string) => {
|
|
||||||
return text
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateEmailContent = (data: Record<string, string>) => {
|
const generateEmailContent = (data: Record<string, string>) => {
|
||||||
const stringData = Object.entries(data).reduce(
|
const stringData = Object.entries(data).reduce(
|
||||||
(str, [key, val]) =>
|
(str, [key, val]) =>
|
||||||
str +
|
str +
|
||||||
`${CONTACT_MESSAGE_FIELDS[key] || key}: ${val.replace(/\n/g, "\n")} \n\n`,
|
`${CONTACT_MESSAGE_FIELDS[key] || key}: ${val.replace(/\n/g, "\n")} \n\n`,
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
const htmlData = Object.entries(data).reduce(
|
const htmlData = Object.entries(data).reduce(
|
||||||
(str, [key, val]) =>
|
(str, [key, val]) =>
|
||||||
str +
|
str +
|
||||||
`<h3 class="form-heading">${escapeHtml(
|
(key == "captchaToken"
|
||||||
CONTACT_MESSAGE_FIELDS[key] || key
|
? ""
|
||||||
)}</h3><p class="form-answer">${escapeHtml(val).replace(
|
: `<h3 class="form-heading">${escape(
|
||||||
/\n/g,
|
CONTACT_MESSAGE_FIELDS[key] || key
|
||||||
"<br/>"
|
)}</h3><p class="form-answer">${escape(val).replace(
|
||||||
)}</p>`,
|
/\n/g,
|
||||||
""
|
"<br/>"
|
||||||
);
|
)}</p>`),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: stringData,
|
text: stringData,
|
||||||
html: `<!DOCTYPE html>
|
html: `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Contact Email</title>
|
<title>Contact Email</title>
|
||||||
|
@ -90,37 +85,98 @@ const generateEmailContent = (data: Record<string, string>) => {
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>`,
|
</html>`
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Method Not Allowed" },
|
{ message: "Method Not Allowed" },
|
||||||
{ status: 405 }
|
{ status: 405 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await req.json();
|
const data = await req.json();
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
await transporter.sendMail({
|
// Verify hCaptcha token
|
||||||
...mailOptions,
|
const hcaptchaResponse = await fetch(
|
||||||
...generateEmailContent(data),
|
`https://api.hcaptcha.com/siteverify`,
|
||||||
subject: "Contact Email",
|
{
|
||||||
});
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: `secret=${process.env.HCAPTCHA_SECRET}&response=${data.captchaToken}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return NextResponse.json(
|
const hcaptchaData = await hcaptchaResponse.json();
|
||||||
{ message: "Email sent successfully" },
|
|
||||||
{ status: 200 }
|
if (!hcaptchaData.success) {
|
||||||
);
|
return NextResponse.json(
|
||||||
} catch (error) {
|
{ message: "Captcha verification failed." },
|
||||||
console.error("Error sending email:", error);
|
{ status: 400 }
|
||||||
return NextResponse.json(
|
);
|
||||||
{ message: "Internal Server Error" },
|
}
|
||||||
{ status: 500 }
|
|
||||||
);
|
// Check email address
|
||||||
}
|
if (!isEmail(data.email)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Invalid email address" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check email host
|
||||||
|
const emailDomainMatch = data.email.match(/@([^@]+)/);
|
||||||
|
const emailDomain = emailDomainMatch ? emailDomainMatch[1] : "";
|
||||||
|
let isEmailHostValid = false;
|
||||||
|
try {
|
||||||
|
const mxRecords = await dns.resolveMx(emailDomain);
|
||||||
|
if (mxRecords.length > 0) {
|
||||||
|
for (let i = 0; i < mxRecords.length; i++) {
|
||||||
|
try {
|
||||||
|
const aRecords = await dns.resolve4(mxRecords[i].exchange);
|
||||||
|
if (aRecords.length > 0) {
|
||||||
|
isEmailHostValid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
try {
|
||||||
|
const aaaaRecords = await dns.resolve6(mxRecords[i].exchange);
|
||||||
|
if (aaaaRecords.length > 0) {
|
||||||
|
isEmailHostValid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
if (!isEmailHostValid) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Email domain is misconfigured" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,29 +2,31 @@
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const collection = db.collection("downloads");
|
const collection = db.collection("downloads");
|
||||||
|
|
||||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||||
|
|
||||||
if (result.deletedCount === 1) {
|
if (result.deletedCount === 1) {
|
||||||
return NextResponse.json({ message: "Log deleted successfully" });
|
revalidatePath("/downloads");
|
||||||
} else {
|
return NextResponse.json({ message: "Log deleted successfully" });
|
||||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
} else {
|
||||||
}
|
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||||
} catch (error) {
|
}
|
||||||
return NextResponse.json(
|
} catch (error) {
|
||||||
{ message: "Failed to delete log", error: error },
|
return NextResponse.json(
|
||||||
{ status: 500 }
|
{ message: "Failed to delete log", error: error },
|
||||||
);
|
{ status: 500 }
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,29 +2,31 @@
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const collection = db.collection("logs");
|
const collection = db.collection("logs");
|
||||||
|
|
||||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||||
|
|
||||||
if (result.deletedCount === 1) {
|
if (result.deletedCount === 1) {
|
||||||
return NextResponse.json({ message: "Log deleted successfully" });
|
revalidatePath("/changelog");
|
||||||
} else {
|
return NextResponse.json({ message: "Log deleted successfully" });
|
||||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
} else {
|
||||||
}
|
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||||
} catch (error) {
|
}
|
||||||
return NextResponse.json(
|
} catch (error) {
|
||||||
{ message: "Failed to delete log", error: error },
|
return NextResponse.json(
|
||||||
{ status: 500 }
|
{ message: "Failed to delete log", error: error },
|
||||||
);
|
{ status: 500 }
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,29 +2,31 @@
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const collection = db.collection("mods");
|
const collection = db.collection("mods");
|
||||||
|
|
||||||
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
||||||
|
|
||||||
if (result.deletedCount === 1) {
|
if (result.deletedCount === 1) {
|
||||||
return NextResponse.json({ message: "Log deleted successfully" });
|
revalidatePath("/mods");
|
||||||
} else {
|
return NextResponse.json({ message: "Log deleted successfully" });
|
||||||
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
} else {
|
||||||
}
|
return NextResponse.json({ message: "Log not found" }, { status: 404 });
|
||||||
} catch (error) {
|
}
|
||||||
return NextResponse.json(
|
} catch (error) {
|
||||||
{ message: "Failed to delete log", error: error },
|
return NextResponse.json(
|
||||||
{ status: 500 }
|
{ message: "Failed to delete log", error: error },
|
||||||
);
|
{ status: 500 }
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
40
app/api/delete/vulnerability/[id]/route.ts
Normal file
40
app/api/delete/vulnerability/[id]/route.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import { ObjectId } from "mongodb";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ message: "ID is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
const result = await db
|
||||||
|
.collection("vulnerabilities")
|
||||||
|
.deleteOne({ _id: new ObjectId(id) });
|
||||||
|
if (result.deletedCount === 1) {
|
||||||
|
revalidatePath("/vulnerabilities");
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ export const dynamic = "force-dynamic";
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const downloads = await db.collection("downloads").find().toArray();
|
const downloads = await db.collection("downloads").find().toArray();
|
||||||
// console.log("Downloads fetched:", downloads);
|
// console.log("Downloads fetched:", downloads);
|
||||||
return NextResponse.json(downloads, { status: 200 });
|
return NextResponse.json(downloads, { status: 200 });
|
||||||
|
|
|
@ -5,30 +5,30 @@ import { serialize } from "cookie";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const { username, password } = await request.json();
|
const { username, password } = await request.json();
|
||||||
|
|
||||||
const adminUsername = process.env.ADMIN_USERNAME;
|
const adminUsername = process.env.ADMIN_USERNAME;
|
||||||
const adminPassword = process.env.ADMIN_PASSWORD;
|
const adminPassword = process.env.ADMIN_PASSWORD;
|
||||||
|
|
||||||
if (username === adminUsername && password === adminPassword) {
|
if (username === adminUsername && password === adminPassword) {
|
||||||
const cookie = serialize("auth", "authenticated", {
|
const cookie = serialize("auth", "authenticated", {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: "/",
|
path: "/",
|
||||||
maxAge: 60 * 60 * 24, // 1 day
|
maxAge: 60 * 60 * 24 // 1 day
|
||||||
});
|
});
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify({ message: "Login successful" }), {
|
return new NextResponse(JSON.stringify({ message: "Login successful" }), {
|
||||||
headers: {
|
headers: {
|
||||||
"Set-Cookie": cookie,
|
"Set-Cookie": cookie,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), {
|
return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), {
|
||||||
status: 401,
|
status: 401,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,15 @@ export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
// Handler for GET requests
|
// Handler for GET requests
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const downloads = await db.collection("logs").find().toArray();
|
const downloads = await db.collection("logs").find().toArray();
|
||||||
return NextResponse.json(downloads, { status: 200 });
|
return NextResponse.json(downloads, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch logs" },
|
{ error: "Failed to fetch logs" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,127 +1,112 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { slug: string } }
|
{ params }: { params: { slug: string } }
|
||||||
) => {
|
) => {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db();
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const { slug } = params;
|
const { slug } = params;
|
||||||
|
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = await db.collection("pages").findOne({ slug });
|
const page = await db.collection("pages").findOne({ slug });
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
return NextResponse.json(page, { status: 200 });
|
return NextResponse.json(page, { status: 200 });
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PUT = async (
|
export const PUT = async (
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { slug: string } }
|
{ params }: { params: { slug: string } }
|
||||||
) => {
|
) => {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db();
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const { slug } = params;
|
const { slug } = params;
|
||||||
|
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, content } = await req.json();
|
const { title, content, vulnerabilities } = await req.json();
|
||||||
|
|
||||||
if (typeof title !== "string" || typeof content !== "string") {
|
if (
|
||||||
return NextResponse.json(
|
typeof title !== "string" ||
|
||||||
{ message: "Invalid title or content" },
|
typeof content !== "string" ||
|
||||||
{ status: 400 }
|
typeof vulnerabilities !== "string"
|
||||||
);
|
) {
|
||||||
}
|
return NextResponse.json(
|
||||||
|
{ message: "Invalid title, content, or vulnerabilities" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// it works here ig
|
const result = await db.collection("pages").findOneAndUpdate(
|
||||||
const result = await db
|
{ slug },
|
||||||
.collection("pages")
|
{ $set: { title, content, vulnerabilities } },
|
||||||
.findOneAndUpdate(
|
{ returnDocument: "after" } // Updated option
|
||||||
{ slug },
|
);
|
||||||
{ $set: { title, content } },
|
|
||||||
{ returnDocument: "after" }
|
|
||||||
);
|
|
||||||
|
|
||||||
// i hate my life fr fr
|
if (result?.value) {
|
||||||
console.log("Update Result:", result);
|
const serializedResult = {
|
||||||
// result returns like
|
...result.value,
|
||||||
|
_id: result.value._id.toString() // Convert ObjectId to string
|
||||||
// Update Result: {
|
};
|
||||||
// _id: new ObjectId('66a2946b2b91eef505eef943'),
|
revalidatePath(`/changelog/${slug}`);
|
||||||
// title: 'TEST PAGE',
|
revalidatePath("/vulnerabilities");
|
||||||
// slug: 'test-page',
|
revalidatePath("/sitemap.xml");
|
||||||
// content: 'asd]---\n' +
|
return NextResponse.json(serializedResult, { status: 200 });
|
||||||
// '---\n' +
|
} else {
|
||||||
// '\n' +
|
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||||
// 'this is basic heading ?\n' +
|
}
|
||||||
// '\n' +
|
} catch (error) {
|
||||||
// '**HELLO**\n' +
|
console.error("Error updating page:", error);
|
||||||
// '\n' +
|
return NextResponse.json(
|
||||||
// 'erw\n' +
|
{ message: "Failed to update page" },
|
||||||
// '\n' +
|
{ status: 500 }
|
||||||
// 'trying another time for test'
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// ERRROR : TypeError: Cannot read properties of undefined (reading '_id')
|
|
||||||
// aposdjaoi sdio JUST WORK NIAWWWWWWWWW
|
|
||||||
|
|
||||||
// if (result && result.value) {
|
|
||||||
const serializedResult = {
|
|
||||||
...result?.value,
|
|
||||||
_id: result?.value._id.toString(), // Convert ObjectId to string
|
|
||||||
};
|
|
||||||
return NextResponse.json(result?.value.content, { status: 200 });
|
|
||||||
// } else {
|
|
||||||
// return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
|
||||||
// }
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating page:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: "Failed to update page" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DELETE = async (
|
export const DELETE = async (
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { slug: string } }
|
{ params }: { params: { slug: string } }
|
||||||
) => {
|
) => {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db();
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const { slug } = params;
|
const { slug } = params;
|
||||||
|
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db.collection("pages").deleteOne({ slug });
|
const result = await db.collection("pages").deleteOne({ slug });
|
||||||
|
|
||||||
if (result.deletedCount > 0) {
|
if (result.deletedCount > 0) {
|
||||||
return NextResponse.json(
|
revalidatePath(`/changelog/${slug}`);
|
||||||
{ message: "Page deleted successfully" },
|
revalidatePath("/vulnerabilities");
|
||||||
{ status: 200 }
|
revalidatePath("/sitemap.xml");
|
||||||
);
|
return NextResponse.json(
|
||||||
} else {
|
{ message: "Page deleted successfully" },
|
||||||
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
{ status: 200 }
|
||||||
}
|
);
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error("Error deleting page:", error);
|
return NextResponse.json({ message: "Page not found" }, { status: 404 });
|
||||||
return NextResponse.json(
|
}
|
||||||
{ message: "Failed to delete page" },
|
} catch (error) {
|
||||||
{ status: 500 }
|
console.error("Error deleting page:", error);
|
||||||
);
|
return NextResponse.json(
|
||||||
}
|
{ message: "Failed to delete page" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,43 +1,47 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export const GET = async (req: NextRequest) => {
|
export const GET = async (req: NextRequest) => {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db();
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pages = await db.collection("pages").find().toArray();
|
const pages = await db.collection("pages").find().toArray();
|
||||||
return NextResponse.json(pages, { status: 200 });
|
return NextResponse.json(pages, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pages:", error);
|
console.error("Error fetching pages:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Failed to fetch pages" },
|
{ message: "Failed to fetch pages" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const POST = async (req: NextRequest) => {
|
export const POST = async (req: NextRequest) => {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db();
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const { title, slug, content } = await req.json();
|
const { title, slug, content } = await req.json();
|
||||||
|
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Missing required fields" },
|
{ message: "Missing required fields" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newPage = { title, slug, content };
|
const newPage = { title, slug, content };
|
||||||
const result = await db.collection("pages").insertOne(newPage);
|
const result = await db.collection("pages").insertOne(newPage);
|
||||||
return NextResponse.json(newPage, { status: 201 });
|
revalidatePath(`/changelog/${slug}`);
|
||||||
} catch (error) {
|
revalidatePath("/vulnerabilities");
|
||||||
console.error("Error creating page:", error);
|
revalidatePath("/sitemap.xml");
|
||||||
return NextResponse.json(
|
return NextResponse.json(newPage, { status: 201 });
|
||||||
{ message: "Failed to create page" },
|
} catch (error) {
|
||||||
{ status: 500 }
|
console.error("Error creating page:", error);
|
||||||
);
|
return NextResponse.json(
|
||||||
}
|
{ message: "Failed to create page" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const dynamic = "force-dynamic";
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const downloads = await db.collection("mods").find().toArray();
|
const downloads = await db.collection("mods").find().toArray();
|
||||||
return NextResponse.json(downloads, { status: 200 });
|
return NextResponse.json(downloads, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
64
app/api/newsletter/send/route.ts
Normal file
64
app/api/newsletter/send/route.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.EMAIL_SERVER,
|
||||||
|
port: parseInt(process.env.EMAIL_PORT ? process.env.EMAIL_PORT : "25"),
|
||||||
|
secure: Boolean(process.env.EMAIL_SECURE ? process.env.EMAIL_SECURE : false),
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendEmail = async (
|
||||||
|
to: { email: string; unsubscribeId: string }[],
|
||||||
|
subject: string,
|
||||||
|
html: string
|
||||||
|
) => {
|
||||||
|
for (let i = 0; i < to.length; i++) {
|
||||||
|
try {
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: process.env.EMAIL_NEWSLETTER_ADDRESS,
|
||||||
|
to: to[i].email,
|
||||||
|
subject: subject,
|
||||||
|
html: html.replace(
|
||||||
|
/\{unsubscribeId\}/g,
|
||||||
|
encodeURIComponent(to[i].unsubscribeId)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} 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();
|
||||||
|
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
const collection = db.collection("subscribers");
|
||||||
|
|
||||||
|
const subscribers = await collection
|
||||||
|
.find({}, { projection: { email: 1, unsubscribeId: 1 } })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
if (subscribers.length === 0) {
|
||||||
|
console.error("No subscribers found in the database.");
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "No subscribers found." },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendEmail(subscribers as any[], subject, html ?? "No HTML specified");
|
||||||
|
return NextResponse.json({ message: "Emails sent successfully" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling POST request:", error);
|
||||||
|
return NextResponse.error();
|
||||||
|
}
|
||||||
|
}
|
44
app/api/newsletter/subscriber/route.ts
Normal file
44
app/api/newsletter/subscriber/route.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
|
||||||
|
interface Subscriber {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
const collection = db.collection("subscribers");
|
||||||
|
|
||||||
|
// 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 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 });
|
||||||
|
}
|
||||||
|
}
|
52
app/api/newsletter/test/route.ts
Normal file
52
app/api/newsletter/test/route.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.EMAIL_SERVER,
|
||||||
|
port: parseInt(process.env.EMAIL_PORT ? process.env.EMAIL_PORT : "25"),
|
||||||
|
secure: Boolean(process.env.EMAIL_SECURE ? process.env.EMAIL_SECURE : false),
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendEmail = async (to: string[], subject: string, html: string) => {
|
||||||
|
for (let i = 0; i < to.length; i++) {
|
||||||
|
try {
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: process.env.EMAIL_NEWSLETTER_ADDRESS,
|
||||||
|
to: to[i],
|
||||||
|
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();
|
||||||
|
|
||||||
|
const testEmails = process.env.EMAIL_NEWSLETTER_TESTDEST
|
||||||
|
? process.env.EMAIL_NEWSLETTER_TESTDEST.split(",")
|
||||||
|
: [];
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
80
app/api/revalidate/route.ts
Normal file
80
app/api/revalidate/route.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* This code is responsible for revalidating the cache when a post or author is updated.
|
||||||
|
*
|
||||||
|
* It is set up to receive a validated GROQ-powered Webhook from Sanity.io:
|
||||||
|
* https://www.sanity.io/docs/webhooks
|
||||||
|
*
|
||||||
|
* 1. Go to the API section of your Sanity project on sanity.io/manage or run `npx sanity hook create`
|
||||||
|
* 2. Click "Create webhook"
|
||||||
|
* 3. Set the URL to https://YOUR_NEXTJS_SITE_URL/api/revalidate
|
||||||
|
* 4. Dataset: Choose desired dataset or leave at default "all datasets"
|
||||||
|
* 5. Trigger on: "Create", "Update", and "Delete"
|
||||||
|
* 6. Filter: _type == "blog"
|
||||||
|
* 7. Projection: Leave empty
|
||||||
|
* 8. Status: Enable webhook
|
||||||
|
* 9. HTTP method: POST
|
||||||
|
* 10. HTTP Headers: Leave empty
|
||||||
|
* 11. API version: v2023-08-08
|
||||||
|
* 12. Include drafts: No
|
||||||
|
* 13. Secret: Set to the same value as SANITY_REVALIDATE_SECRET (create a random secret if you haven't yet)
|
||||||
|
* 14. Save the cofiguration
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isValidSignature, SIGNATURE_HEADER_NAME } from "@sanity/webhook";
|
||||||
|
import { client } from "@/lib/sanity";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const secret = `${process.env.SANITY_WEBHOOK_SECRET}`;
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const body = await req.json();
|
||||||
|
const rawBody = JSON.stringify(body);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isValidSignature(
|
||||||
|
rawBody,
|
||||||
|
req.headers.get(SIGNATURE_HEADER_NAME) ?? "",
|
||||||
|
secret.trim()
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return NextResponse.json({ message: "Invalid signature" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (body._type == "blog") {
|
||||||
|
if (body.slug.current) {
|
||||||
|
revalidatePath(`/blog/${body.slug.current}`);
|
||||||
|
revalidatePath("/sitemap.xml");
|
||||||
|
revalidatePath("/rss.xml");
|
||||||
|
}
|
||||||
|
revalidatePath("/blog");
|
||||||
|
|
||||||
|
// Change in /blog/page/[id] route and in BlogCards component too!
|
||||||
|
const cardsPerPage = 6;
|
||||||
|
|
||||||
|
const totalPostsQuery = `count(*[_type == 'blog'])`;
|
||||||
|
const totalPosts: number = await client.fetch(
|
||||||
|
totalPostsQuery,
|
||||||
|
{},
|
||||||
|
{ cache: "no-store" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalPosts / cardsPerPage);
|
||||||
|
for (let i = 1; i <= totalPages + 1; i++) {
|
||||||
|
revalidatePath(`/blog/page/${i.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: `Revalidated "${body._type}" with slug "${body.slug.current}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "No managed type" });
|
||||||
|
} catch (err) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Error revalidating" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
134
app/api/subscribe/route.ts
Normal file
134
app/api/subscribe/route.ts
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import { Collection } from "mongodb";
|
||||||
|
import dns from "dns/promises";
|
||||||
|
import { isEmail } from "validator";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const generateUnsubscribeID = () => {
|
||||||
|
const chars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < 32; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUniqueUnsubscribeID = async (collection: Collection) => {
|
||||||
|
const id: string = generateUnsubscribeID();
|
||||||
|
const result = await collection
|
||||||
|
.find({
|
||||||
|
unsubscribeId: id
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
if (result.length > 0) {
|
||||||
|
const newId: string = await generateUniqueUnsubscribeID(collection);
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { email, captchaToken } = await req.json();
|
||||||
|
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const hcaptchaData = await hcaptchaResponse.json();
|
||||||
|
|
||||||
|
if (!hcaptchaData.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Captcha verification failed." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check email address
|
||||||
|
if (!isEmail(email)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Invalid email address" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check email host
|
||||||
|
const emailDomainMatch = email.match(/@([^@]+)/);
|
||||||
|
const emailDomain = emailDomainMatch ? emailDomainMatch[1] : "";
|
||||||
|
let isEmailHostValid = false;
|
||||||
|
try {
|
||||||
|
const mxRecords = await dns.resolveMx(emailDomain);
|
||||||
|
if (mxRecords.length > 0) {
|
||||||
|
for (let i = 0; i < mxRecords.length; i++) {
|
||||||
|
try {
|
||||||
|
const aRecords = await dns.resolve4(mxRecords[i].exchange);
|
||||||
|
if (aRecords.length > 0) {
|
||||||
|
isEmailHostValid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
try {
|
||||||
|
const aaaaRecords = await dns.resolve6(mxRecords[i].exchange);
|
||||||
|
if (aaaaRecords.length > 0) {
|
||||||
|
isEmailHostValid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
if (!isEmailHostValid) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Email domain is misconfigured" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// saves the email in the db
|
||||||
|
await collection.insertOne({
|
||||||
|
email,
|
||||||
|
subscribedAt: new Date(),
|
||||||
|
unsubscribeId: await generateUniqueUnsubscribeID(collection)
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Successfully subscribed!" },
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error subscribing:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
55
app/api/unsubscribe/route.ts
Normal file
55
app/api/unsubscribe/route.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import { Collection } from "mongodb";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { unsubscribeId, captchaToken } = await req.json();
|
||||||
|
|
||||||
|
if (!unsubscribeId || !captchaToken) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Unsubscription ID 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}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const hcaptchaData = await hcaptchaResponse.json();
|
||||||
|
|
||||||
|
if (!hcaptchaData.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Captcha verification failed." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
const collection = db.collection("subscribers");
|
||||||
|
|
||||||
|
const result = await collection.deleteOne({ unsubscribeId });
|
||||||
|
|
||||||
|
if (result.deletedCount === 1) {
|
||||||
|
return NextResponse.json({ message: "Unsubscribed successfully" });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ message: "Not subscribed" }, { status: 404 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error subscribing:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
app/api/update/mods/[id]/route.ts
Normal file
49
app/api/update/mods/[id]/route.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import clientPromise from "@/lib/db";
|
||||||
|
import { ObjectId } from "mongodb";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function PUT(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
const { id } = params;
|
||||||
|
const body = await request.json();
|
||||||
|
const { fileName, version, downloadLink, fileSize } = body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
|
const result = await db.collection("mods").updateOne(
|
||||||
|
{ _id: new ObjectId(id) },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
date: new Date().toISOString().split("T")[0],
|
||||||
|
fileName,
|
||||||
|
version,
|
||||||
|
downloadLink,
|
||||||
|
fileSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.modifiedCount > 0) {
|
||||||
|
revalidatePath("/mods");
|
||||||
|
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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
// Force the API to use SSR instead of static generation
|
// Force the API to use SSR instead of static generation
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
@ -9,15 +10,17 @@ export async function POST(request: Request) {
|
||||||
const { fileName, version, downloadLink, fileSize } = body;
|
const { fileName, version, downloadLink, fileSize } = body;
|
||||||
|
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
const result = await db.collection("downloads").insertOne({
|
const result = await db.collection("downloads").insertOne({
|
||||||
date: new Date().toISOString().split("T")[0],
|
date: new Date().toISOString().split("T")[0],
|
||||||
fileName,
|
fileName,
|
||||||
version,
|
version,
|
||||||
downloadLink,
|
downloadLink,
|
||||||
fileSize,
|
fileSize
|
||||||
});
|
});
|
||||||
|
|
||||||
|
revalidatePath("/downloads");
|
||||||
|
|
||||||
return NextResponse.json({ success: true, id: result.insertedId });
|
return NextResponse.json({ success: true, id: result.insertedId });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
// Force the API to use SSR instead of static generation
|
// Force the API to use SSR instead of static generation
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { version, date, bullets } = body;
|
const { version, date, bullets } = body;
|
||||||
|
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
const result = await db.collection("logs").insertOne({
|
const result = await db.collection("logs").insertOne({
|
||||||
version,
|
version,
|
||||||
date,
|
date,
|
||||||
bullets,
|
bullets
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true, id: result.insertedId });
|
revalidatePath("/changelog");
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, id: result.insertedId });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
// Force the API to use SSR instead of static generation
|
// Force the API to use SSR instead of static generation
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
@ -9,15 +10,17 @@ export async function POST(request: Request) {
|
||||||
const { fileName, version, downloadLink, fileSize } = body;
|
const { fileName, version, downloadLink, fileSize } = body;
|
||||||
|
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
const result = await db.collection("mods").insertOne({
|
const result = await db.collection("mods").insertOne({
|
||||||
date: new Date().toISOString().split("T")[0],
|
date: new Date().toISOString().split("T")[0],
|
||||||
fileName,
|
fileName,
|
||||||
version,
|
version,
|
||||||
downloadLink,
|
downloadLink,
|
||||||
fileSize,
|
fileSize
|
||||||
});
|
});
|
||||||
|
|
||||||
|
revalidatePath("/mods");
|
||||||
|
|
||||||
return NextResponse.json({ success: true, id: result.insertedId });
|
return NextResponse.json({ success: true, id: result.insertedId });
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,17 +6,11 @@ const f = createUploadthing();
|
||||||
// const auth = (req: Request) => ({ id: "fakeId" });
|
// const auth = (req: Request) => ({ id: "fakeId" });
|
||||||
|
|
||||||
export const ourFileRouter = {
|
export const ourFileRouter = {
|
||||||
imageUploader: f({ "application/zip": { maxFileSize: "8MB" } })
|
imageUploader: f({
|
||||||
// .middleware(async ({ req }) => {
|
"application/zip": { maxFileSize: "8MB" }
|
||||||
// const user = await auth(req);
|
}).onUploadComplete(async ({ metadata, file }) => {
|
||||||
// if (!user) throw new UploadThingError("Unauthorized");
|
console.log("file url", file.url);
|
||||||
// return { userId: user.id };
|
})
|
||||||
// })
|
|
||||||
.onUploadComplete(async ({ metadata, file }) => {
|
|
||||||
// console.log("Upload complete for userId:", metadata.userId);
|
|
||||||
console.log("file url", file.url);
|
|
||||||
// return { uploadedBy: metadata.userId };
|
|
||||||
}),
|
|
||||||
} satisfies FileRouter;
|
} satisfies FileRouter;
|
||||||
|
|
||||||
export type OurFileRouter = typeof ourFileRouter;
|
export type OurFileRouter = typeof ourFileRouter;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { createRouteHandler } from "uploadthing/next";
|
import { createRouteHandler } from "uploadthing/next";
|
||||||
import { ourFileRouter } from "./core";
|
import { ourFileRouter } from "./core";
|
||||||
|
|
||||||
// Force the API to use SSR instead of static generation
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export const { GET, POST } = createRouteHandler({
|
export const { GET, POST } = createRouteHandler({
|
||||||
router: ourFileRouter,
|
router: ourFileRouter
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import clientPromise from "@/lib/db";
|
import clientPromise from "@/lib/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
// Force the API to use SSR instead of static generation
|
// Force the API to use SSR instead of static generation
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { version, date, bullets } = body;
|
const { version, date, bullets } = body;
|
||||||
|
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
const result = await db.collection("vulnerabilities").insertOne({
|
const result = await db.collection("vulnerabilities").insertOne({
|
||||||
version,
|
version,
|
||||||
bullets,
|
bullets
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true, id: result.insertedId });
|
revalidatePath("/vulnerabilities");
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, id: result.insertedId });
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,15 @@ export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
// Handler for GET requests
|
// Handler for GET requests
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db("downloadsDatabase");
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
const downloads = await db.collection("vulnerabilities").find().toArray();
|
const downloads = await db.collection("vulnerabilities").find().toArray();
|
||||||
return NextResponse.json(downloads, { status: 200 });
|
return NextResponse.json(downloads, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch logs" },
|
{ error: "Failed to fetch logs" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -2,39 +2,62 @@ import type { Metadata } from "next";
|
||||||
import { Poppins } from "next/font/google";
|
import { Poppins } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ThemeProvider } from "@/components/shared/providers/themeprovider";
|
import { ThemeProvider } from "@/components/shared/providers/themeprovider";
|
||||||
import AuthProvider from "@/components/shared/providers/AuthProvider";
|
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
import Analytics from "@/components/shared/providers/Analytics";
|
||||||
|
|
||||||
const poppins = Poppins({
|
const poppins = Poppins({
|
||||||
weight: ["400", "600", "700", "900"],
|
weight: ["400", "600", "700", "900"],
|
||||||
subsets: ["latin"],
|
subsets: ["latin"]
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "SVRJS - A Web Server running on Nodejs",
|
title: "SVR.JS - a web server running on Node.JS",
|
||||||
description: "Open Source Software Library",
|
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: "SVR.JS - 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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}`,
|
||||||
|
type: "website",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
alt: "SVR.JS - a web server running on Node.JS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
site: "@SVR_JS",
|
||||||
|
title: "SVR.JS - 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: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
|
||||||
|
creator: "@SVR_JS"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en-US" suppressHydrationWarning>
|
||||||
<body className={`antialiased ${poppins.className}`}>
|
<body className={`antialiased ${poppins.className}`}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="dark"
|
enableSystem={true}
|
||||||
enableSystem
|
disableTransitionOnChange
|
||||||
disableTransitionOnChange
|
>
|
||||||
>
|
{children}
|
||||||
<AuthProvider>
|
<Toaster />
|
||||||
{children}
|
<Analytics pagesRouter={false} />
|
||||||
<Toaster />
|
</ThemeProvider>
|
||||||
</AuthProvider>
|
</body>
|
||||||
</ThemeProvider>
|
</html>
|
||||||
</body>
|
);
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
10
app/login/layout.tsx
Normal file
10
app/login/layout.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from "react";
|
||||||
|
import AuthProvider from "@/components/shared/providers/AuthProvider";
|
||||||
|
|
||||||
|
export default function AdminLayout({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <AuthProvider>{children}</AuthProvider>;
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ const LoginPage = () => {
|
||||||
const res = await signIn("credentials", {
|
const res = await signIn("credentials", {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
username,
|
username,
|
||||||
password,
|
password
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res?.ok) {
|
if (res?.ok) {
|
||||||
|
@ -42,7 +42,7 @@ const LoginPage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl h-screen flex justify-center bg-gray-900 flex-col container">
|
<div className="max-w-xl h-screen flex justify-center bg-gray-900 flex-col container">
|
||||||
<h1 className="text-3xl font-bold mb-4">SVRJS ADMIN PANEL</h1>
|
<h1 className="text-3xl font-bold mb-4">SVR.JS ADMIN PANEL</h1>
|
||||||
{error && <p className="text-red-500 mb-4">{error}</p>}
|
{error && <p className="text-red-500 mb-4">{error}</p>}
|
||||||
<form onSubmit={handleLogin}>
|
<form onSubmit={handleLogin}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|
|
@ -1,18 +1,42 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import Footer from "@/components/shared/Footer";
|
||||||
|
import Navbar from "@/components/shared/Navbar";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import NoScript from "@/components/shared/NoScript";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "404 Not Found - SVR.JS",
|
||||||
|
openGraph: {
|
||||||
|
title: "404 Not Found - SVR.JS"
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
title: "404 Not Found - SVR.JS"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const NotFound = () => {
|
const NotFound = () => {
|
||||||
return (
|
return (
|
||||||
<section id="404error" className="flex-center flex-col wrapper container">
|
<>
|
||||||
<h1 className="text-3xl md:text-5xl text-center">
|
<div className="flex flex-col min-h-screen">
|
||||||
<span className="text-red-500">404</span> Page not Found
|
<Navbar />
|
||||||
</h1>
|
<NoScript />
|
||||||
<p className="text-lg mt-3 text-muted-foreground">
|
<section
|
||||||
Please return back to{" "}
|
id="404error"
|
||||||
<Link href="/" className="underline font-bold">
|
className="flex-center flex-col wrapper container flex-1 flex-grow"
|
||||||
Home
|
>
|
||||||
</Link>
|
<h1 className="text-3xl md:text-5xl text-center">
|
||||||
</p>
|
<span className="text-red-500">404</span> Page not Found
|
||||||
</section>
|
</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 />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
12
app/robots.ts
Normal file
12
app/robots.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
userAgent: "*",
|
||||||
|
allow: "/",
|
||||||
|
disallow: "/unsubscribe"
|
||||||
|
},
|
||||||
|
sitemap: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/sitemap.xml`
|
||||||
|
};
|
||||||
|
}
|
49
app/rss.xml/route.ts
Normal file
49
app/rss.xml/route.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import RSS from "rss";
|
||||||
|
import { client, urlFor } from "@/lib/sanity";
|
||||||
|
import { toHTML } from "@portabletext/to-html";
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const postsQuery = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||||
|
title,
|
||||||
|
"slug": slug.current,
|
||||||
|
content,
|
||||||
|
titleImage,
|
||||||
|
_createdAt
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const posts = await client.fetch(postsQuery);
|
||||||
|
|
||||||
|
const feed = new RSS({
|
||||||
|
title: "SVR.JS Blog",
|
||||||
|
description: "Explore the latest blog posts from SVR.JS",
|
||||||
|
feed_url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/rss.xml`,
|
||||||
|
site_url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}`,
|
||||||
|
image_url: `${process.env.NEXT_PUBLIC_WEBSITE_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: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog/${post.slug}`,
|
||||||
|
date: new Date(post._createdAt).toUTCString(),
|
||||||
|
enclosure: {
|
||||||
|
url: post.titleImage
|
||||||
|
? urlFor(post.titleImage).url()
|
||||||
|
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog-missing.png`
|
||||||
|
},
|
||||||
|
author: "SVR.JS"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new NextResponse(feed.xml({ indent: true }), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,20 +1,73 @@
|
||||||
export default async function sitemap() {
|
import { getAllBlogPostSlugs } from "@/lib/getBlogPost";
|
||||||
let routes = [
|
import clientPromise from "@/lib/db";
|
||||||
"",
|
|
||||||
"/blog",
|
|
||||||
"/changelogs",
|
|
||||||
"/contact",
|
|
||||||
"/contribute",
|
|
||||||
"/downloads",
|
|
||||||
"/forum",
|
|
||||||
"/mods",
|
|
||||||
"/privacy-policy",
|
|
||||||
"/tos",
|
|
||||||
"/vulnerabilities",
|
|
||||||
].map((route) => ({
|
|
||||||
url: `https://vimfn.in${route}`,
|
|
||||||
lastModified: new Date().toISOString().split("T")[0],
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [...routes];
|
export default async function sitemap() {
|
||||||
|
const blogPostSlugs = await getAllBlogPostSlugs();
|
||||||
|
|
||||||
|
const baseRoutes = [
|
||||||
|
"/",
|
||||||
|
"/blog",
|
||||||
|
"/changelog",
|
||||||
|
"/contact",
|
||||||
|
"/contribute",
|
||||||
|
"/downloads",
|
||||||
|
"/mods",
|
||||||
|
"/privacy",
|
||||||
|
"/tos",
|
||||||
|
"/vulnerabilities",
|
||||||
|
"/newsletter",
|
||||||
|
"/docs",
|
||||||
|
"/docs/mod-notes",
|
||||||
|
"/docs/requirements",
|
||||||
|
"/docs/mods/mod-files",
|
||||||
|
"/docs/mods/introduction",
|
||||||
|
"/docs/mods/mod-loading-order",
|
||||||
|
"/docs/mods/mod-development",
|
||||||
|
"/docs/mods/mod-development-legacy",
|
||||||
|
"/docs/server-side-javascript/svrjs-ssjs",
|
||||||
|
"/docs/server-side-javascript/migration",
|
||||||
|
"/docs/api/svrjs-api-legacy",
|
||||||
|
"/docs/api/svrjs-api",
|
||||||
|
"/docs/config/cgi-scgi-jsgi-php",
|
||||||
|
"/docs/config/cli-options",
|
||||||
|
"/docs/config/reverse-proxy-config",
|
||||||
|
"/docs/config/configuration",
|
||||||
|
"/docs/config/redirects",
|
||||||
|
"/docs/config/forward-proxy-notes",
|
||||||
|
"/docs/config/fastcgi-php-fpm",
|
||||||
|
"/docs/config/user-management",
|
||||||
|
"/docs/config/environment",
|
||||||
|
"/docs/config/virtual-hosts",
|
||||||
|
"/docs/config/http-auth",
|
||||||
|
"/docs/config/page-customization",
|
||||||
|
"/docs/config/client-secure",
|
||||||
|
"/docs/config/custom-error",
|
||||||
|
"/docs/getting-started/svrjs-commands",
|
||||||
|
"/docs/getting-started/updating-svrjs",
|
||||||
|
"/docs/getting-started/svrjs-utilities",
|
||||||
|
"/docs/getting-started/features",
|
||||||
|
"/docs/getting-started/svrjs-files",
|
||||||
|
"/docs/getting-started/installation"
|
||||||
|
].map((route) => ({
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}${route}`,
|
||||||
|
lastModified: new Date().toISOString().split("T")[0]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const blogRoutes = blogPostSlugs.map((slug) => ({
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/blog/${slug.slug}`,
|
||||||
|
lastModified: new Date().toISOString().split("T")[0]
|
||||||
|
}));
|
||||||
|
|
||||||
|
let changelogRoutes: any[] = [];
|
||||||
|
try {
|
||||||
|
const client = await clientPromise;
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
const slugs = await db.collection("pages").find().toArray();
|
||||||
|
changelogRoutes = slugs.map((slug) => ({
|
||||||
|
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/changelog/${slug.slug}`,
|
||||||
|
lastModified: new Date().toISOString().split("T")[0]
|
||||||
|
}));
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
return [...baseRoutes, ...blogRoutes, ...changelogRoutes];
|
||||||
}
|
}
|
||||||
|
|
11
app/studio/[[...index]]/page.tsx
Normal file
11
app/studio/[[...index]]/page.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { NextStudio } from "next-sanity/studio";
|
||||||
|
import React from "react";
|
||||||
|
import config from "@/sanity.config";
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export { metadata, viewport } from "next-sanity/studio";
|
||||||
|
|
||||||
|
export default function StudioPage() {
|
||||||
|
return <NextStudio config={config} />;
|
||||||
|
}
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: ["@commitlint/config-conventional"]
|
||||||
|
};
|
159
components/cards/BlogCards.tsx
Normal file
159
components/cards/BlogCards.tsx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { client, urlFor } from "@/lib/sanity";
|
||||||
|
import { Card, CardContent } from "../ui/card";
|
||||||
|
import {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogCardInterface {
|
||||||
|
page: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlogCards: React.FC<BlogCardInterface> = async (props) => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
// Change in /blog/page/[id] route and in /api/revalidate route too!
|
||||||
|
const cardsPerPage = 6;
|
||||||
|
const currentPage = props.page;
|
||||||
|
|
||||||
|
const query = `*[_type == 'blog'] | order(_createdAt desc) {
|
||||||
|
title,
|
||||||
|
smallDescription,
|
||||||
|
"currentSlug": slug.current,
|
||||||
|
titleImage,
|
||||||
|
_createdAt
|
||||||
|
}[${(currentPage - 1) * cardsPerPage}...${currentPage * cardsPerPage}]`;
|
||||||
|
|
||||||
|
const posts: BlogPostcard[] = await client.fetch(
|
||||||
|
query,
|
||||||
|
{},
|
||||||
|
{ cache: "no-store" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalPostsQuery = `count(*[_type == 'blog'])`;
|
||||||
|
const totalPosts: number = await client.fetch(
|
||||||
|
totalPostsQuery,
|
||||||
|
{},
|
||||||
|
{ cache: "no-store" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalPosts / cardsPerPage);
|
||||||
|
|
||||||
|
let begPage = currentPage - 2;
|
||||||
|
let endPage = currentPage + 2;
|
||||||
|
if (endPage > totalPages) {
|
||||||
|
begPage -= endPage - totalPages;
|
||||||
|
endPage = totalPages;
|
||||||
|
}
|
||||||
|
if (begPage < 1) {
|
||||||
|
endPage += 1 - begPage;
|
||||||
|
begPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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={
|
||||||
|
post.titleImage
|
||||||
|
? urlFor(post.titleImage).url()
|
||||||
|
: "/blog-missing.png"
|
||||||
|
}
|
||||||
|
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">
|
||||||
|
<h2 className="text-xl font-semibold leading-tight">
|
||||||
|
{post.title}
|
||||||
|
</h2>
|
||||||
|
<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={`/blog/page/${currentPage - 1}`} />
|
||||||
|
)}
|
||||||
|
</PaginationItem>
|
||||||
|
{Array.from({ length: totalPages > 5 ? 5 : totalPages }).map(
|
||||||
|
(_, i) => (
|
||||||
|
<PaginationItem key={i}>
|
||||||
|
<PaginationLink
|
||||||
|
href={`/blog/page/${begPage + i}`}
|
||||||
|
isActive={currentPage === begPage + i}
|
||||||
|
>
|
||||||
|
{begPage + i}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<PaginationItem>
|
||||||
|
{currentPage < totalPages && (
|
||||||
|
<PaginationNext href={`/blog/page/${currentPage + 1}`} />
|
||||||
|
)}
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlogCards;
|
30
components/cards/MonacoEditor.tsx
Normal file
30
components/cards/MonacoEditor.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Editor } from "@monaco-editor/react";
|
||||||
|
import { EXAMPLE_A1 } from "@/constants";
|
||||||
|
|
||||||
|
interface CodeEditorProps {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeEditor;
|
|
@ -2,54 +2,54 @@ import Image from "next/image";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface TestimonialCard {
|
interface TestimonialCard {
|
||||||
avatar: string;
|
avatar: string;
|
||||||
name: string;
|
name: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
testimonial: string;
|
testimonial: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TestimonialCard = ({
|
const TestimonialCard = ({
|
||||||
avatar,
|
avatar,
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
testimonial,
|
testimonial,
|
||||||
rating,
|
rating
|
||||||
}: TestimonialCard) => {
|
}: TestimonialCard) => {
|
||||||
return (
|
return (
|
||||||
<li className="inline-block w-full">
|
<li className="inline-block w-full">
|
||||||
<div className="bg-primary/10 dark:bg-[#1c1c1c] mx-auto mb-5 flex w-full cursor-default flex-col gap-4 rounded-2xl px-[30px] py-6 shadow-md transition-all hover:scale-[103%]">
|
<div className="bg-primary/10 dark:bg-[#1c1c1c] mx-auto mb-5 flex w-full cursor-default flex-col gap-4 rounded-2xl px-[30px] py-6 shadow-md transition-all hover:scale-[103%]">
|
||||||
<div className="flex flex-row items-center gap-3">
|
<div className="flex flex-row items-center gap-3">
|
||||||
<div>
|
<div>
|
||||||
<Image
|
<Image
|
||||||
src={`/testimonials/${avatar}.webp`}
|
src={`/testimonials/${avatar}.webp`}
|
||||||
alt="avatar1"
|
alt="avatar1"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div className="small-semibold dark:text-white">{name}</div>
|
<div className="small-semibold dark:text-white">{name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">{role}</div>
|
<div className="text-sm text-muted-foreground">{role}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="body-regular dark:text-white">{testimonial}</p>
|
<p className="body-regular dark:text-white">{testimonial}</p>
|
||||||
<div className="hue-rotate-90 text-lg">
|
<div className="hue-rotate-90 text-lg">
|
||||||
{/* <Image
|
{/* <Image
|
||||||
src="/testimonials/stars.svg"
|
src="/testimonials/stars.svg"
|
||||||
alt="star"
|
alt="star"
|
||||||
width={120}
|
width={120}
|
||||||
height={120}
|
height={120}
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/> */}
|
/> */}
|
||||||
{"⭐".repeat(rating)}
|
{"⭐".repeat(rating)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TestimonialCard;
|
export default TestimonialCard;
|
||||||
|
|
26
components/loader/prismLoader.tsx
Normal file
26
components/loader/prismLoader.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"use client";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import Prism from "prismjs";
|
||||||
|
import "prismjs/themes/prism-okaidia.css";
|
||||||
|
import "prismjs/components/prism-javascript";
|
||||||
|
import "prismjs/components/prism-python";
|
||||||
|
import "prismjs/components/prism-php";
|
||||||
|
import "prismjs/components/prism-bash";
|
||||||
|
import "prismjs/components/prism-sql";
|
||||||
|
import "prismjs/components/prism-yaml";
|
||||||
|
import "prismjs/components/prism-markdown";
|
||||||
|
import "prismjs/components/prism-json";
|
||||||
|
import "prismjs/components/prism-perl";
|
||||||
|
import "prismjs/components/prism-markup";
|
||||||
|
import "prismjs/components/prism-markup-templating";
|
||||||
|
import "prismjs/components/prism-handlebars";
|
||||||
|
|
||||||
|
export default function PrismLoader() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (Prism) {
|
||||||
|
Prism.highlightAll();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -3,40 +3,41 @@ import React from "react";
|
||||||
import Statistics from "./Statistics";
|
import Statistics from "./Statistics";
|
||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
return (
|
return (
|
||||||
<section id="about" className="container py-2 sm:py-9">
|
<section id="about" className="container py-2 sm:py-9">
|
||||||
<div className="bg-accent/50 border rounded-lg py-12">
|
<div className="bg-accent/50 border rounded-lg py-12">
|
||||||
<div className="px-6 flex flex-col-reverse md:flex-row gap-8 md:gap-12">
|
<div className="px-6 flex flex-col-reverse md:flex-row gap-8 md:gap-12">
|
||||||
<Image
|
<Image
|
||||||
src="/about.svg"
|
src="/logo.svg"
|
||||||
alt="aboutpicture"
|
alt="SVR.JS logo"
|
||||||
width={300}
|
width={172}
|
||||||
height={300}
|
height={172}
|
||||||
className="w-[300px] object-contain rounded-lg flex-shrink-0"
|
className="w-[172px] object-contain rounded-lg flex-shrink-0 mx-auto md:pl-6"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col justify-between">
|
<div className="flex flex-col justify-between">
|
||||||
<div className="pb-6">
|
<div className="pb-6">
|
||||||
<h2 className="text-3xl md:text-5xl font-bold">
|
<h2 className="text-3xl md:text-5xl font-bold">
|
||||||
About{" "}
|
About{" "}
|
||||||
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
|
||||||
SVRJS!
|
SVR.JS
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-muted-foreground mt-4">
|
<p className="text-lg text-muted-foreground mt-4">
|
||||||
Host a webpage, run server-side JavaScript, use mods to expand
|
SVR.JS is a web server that runs on top of Node.JS, enabling
|
||||||
server functionality, or use it as a forward or reverse proxy.
|
server-side JavaScript on webpages. SVR.JS also has an
|
||||||
SVRJS is a web server that runs on top of Node.JS, enabling
|
integrated log viewer, log highlighter, and user management
|
||||||
server-side JS on webpages. SVRJS also has an integrated log
|
tool. You can host a webpage using SVR.JS, run server-side
|
||||||
viewer, log highlighter, and user management tool.
|
JavaScript, use mods to expand server functionality, or use it
|
||||||
</p>
|
as a forward or reverse proxy.
|
||||||
</div>
|
</p>
|
||||||
<Statistics />
|
</div>
|
||||||
</div>
|
<Statistics />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default About;
|
export default About;
|
||||||
|
|
50
components/shared/DemoVideo.tsx
Normal file
50
components/shared/DemoVideo.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ArrowUpRight } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import HeroVideoDialog from "../ui/heroVideoAction";
|
||||||
|
|
||||||
|
const DemoVideo = () => {
|
||||||
|
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">
|
||||||
|
SVR.JS
|
||||||
|
</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">
|
||||||
|
<p 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.
|
||||||
|
</p>
|
||||||
|
<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="/svrjs-in-action.png"
|
||||||
|
thumbnailAlt="SVR.JS in action!"
|
||||||
|
/>
|
||||||
|
<hr className="w-full h-1" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DemoVideo;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue