Compare commits

...

No commits in common. "main" and "proxy" have entirely different histories.
main ... proxy

257 changed files with 13082 additions and 31675 deletions

View file

@ -1,5 +1,4 @@
MONGODB_URI=
MONGODB_DB=
UPLOADTHING_SECRET=
UPLOADTHING_APP_ID=
@ -8,26 +7,6 @@ ADMIN_USERNAME=
ADMIN_PASSWORD=
NEXTAUTH_SECRET=
NEXTAUTH_URL=
NEXT_PUBLIC_WEBSITE_URL=
EMAIL_SERVER=
EMAIL_PORT=
EMAIL_SECURE=
EMAIL_USER=
EMAIL=
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=

View file

@ -1,3 +1,3 @@
{
"extends": ["next/core-web-vitals", "prettier", "plugin:prettier/recommended"]
"extends": "next/core-web-vitals"
}

View file

@ -1,13 +0,0 @@
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

View file

@ -1,19 +0,0 @@
# 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 }}

View file

@ -1,2 +0,0 @@
#!/bin/sh
npx --no -- commitlint --edit "$1"

View file

@ -1,2 +0,0 @@
#!/bin/sh
npx lint-staged

7
.swcrc
View file

@ -1,7 +0,0 @@
{
"jsc": {
"keepClassNames": true,
"target": "es2017"
},
"minify": true
}

21
LICENSE
View file

@ -1,21 +0,0 @@
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.

13
actions/login.action.ts Normal file
View file

@ -0,0 +1,13 @@
// 'use server';
// import { NextApiRequest } from 'next';
// import { NextResponse } from 'next/server';
// export async function POST(req: NextApiRequest) {
// const { username, password } = await req.body;
// if (username === process.env.USERNAME && password === process.env.PASSWORD) {
// return NextResponse.json({ success: true });
// } else {
// return NextResponse.json({ success: false });
// }
// }

View file

@ -9,7 +9,7 @@ interface CardProps {
const Card: FC<CardProps> = ({ title, url }) => {
return (
<div className="bg-accent border rounded-lg hover:bg-muted transition-all">
<div className=" bg-zinc-900 border rounded-lg hover:bg-zinc-800 transition-all">
<Link href={url} className="group">
<div className="flex-center rounded-lg p-6">
<h2 className="text-2xl font-bold mb-2">{title}</h2>

View file

@ -25,7 +25,7 @@ const MobileNav = () => {
<>
<Logo width={155} height={53} />
<ul className="header-nav_elements">
{AdminLinks.slice(0, 7).map((link) => {
{AdminLinks.slice(0, 6).map((link) => {
const isActive = link.url === pathname;
return (

View file

@ -17,7 +17,7 @@ const Sidebar = () => {
<nav className="sidebar-nav">
<ul className="sidebar-nav_elements">
{AdminLinks.slice(0, 7).map((link) => {
{AdminLinks.slice(0, 5).map((link) => {
const isActive = link.url === pathname;
return (
@ -37,7 +37,7 @@ const Sidebar = () => {
</ul>
<ul className="sidebar-nav_elements">
{AdminLinks.slice(7).map((link) => {
{AdminLinks.slice(5).map((link) => {
const isActive = link.url === pathname;
return (

View file

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

View file

@ -10,7 +10,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import {
Table,
@ -19,7 +19,7 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { logsSchema } from "@/lib/validations/validation";
@ -46,13 +46,13 @@ const AdminLogPage = () => {
defaultValues: {
version: "",
date: "",
bullets: [{ point: "" }]
}
bullets: [{ point: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "bullets"
name: "bullets",
});
const fetchLogs = async () => {
@ -74,7 +74,7 @@ const AdminLogPage = () => {
const response = await fetch("/api/uploadlogs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
body: JSON.stringify(data),
});
if (response.ok) {
@ -91,7 +91,7 @@ const AdminLogPage = () => {
const deleteLog = async (id: string) => {
try {
const response = await fetch(`/api/delete/logs/${id}`, {
method: "DELETE"
method: "DELETE",
});
if (response.ok) {
fetchLogs();

View file

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

View file

@ -11,7 +11,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { UploadButton } from "@/lib/uploadthing";
@ -23,7 +23,7 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
interface DownloadEntry {
@ -46,14 +46,14 @@ const DownloadsPage = () => {
fileName: "",
version: "",
downloadLink: "",
fileSize: ""
}
fileSize: "",
},
});
const fetchDownloads = async () => {
try {
const response = await fetch("/api/downloads", {
method: "GET"
method: "GET",
});
if (response.ok) {
const data: DownloadEntry[] = await response.json();
@ -82,9 +82,9 @@ const DownloadsPage = () => {
const response = await fetch("/api/upload", {
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(data)
body: JSON.stringify(data),
});
if (response.ok) {
@ -102,7 +102,7 @@ const DownloadsPage = () => {
const deleteDownload = async (id: string) => {
try {
const response = await fetch(`/api/delete/downloads/${id}`, {
method: "DELETE"
method: "DELETE",
});
if (response.ok) {
fetchDownloads();

View file

@ -1,133 +0,0 @@
"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;

View file

@ -1,16 +0,0 @@
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>
);
}

View file

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

View file

@ -11,7 +11,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { UploadButton } from "@/lib/uploadthing";
@ -23,14 +23,8 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
interface ModEntry {
_id: string;
@ -43,56 +37,23 @@ interface ModEntry {
const SvrjsModsAdminPage = () => {
const { toast } = useToast();
const [mods, setMods] = useState<ModEntry[]>([]);
const [editMod, setEditMod] = useState<ModEntry | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const mainForm = useForm<z.infer<typeof modsSchema>>({
const form = useForm<z.infer<typeof modsSchema>>({
resolver: zodResolver(modsSchema),
defaultValues: {
fileName: "",
version: "",
downloadLink: "",
fileSize: ""
}
fileSize: "",
},
});
const dialogForm = useForm<z.infer<typeof modsSchema>>({
resolver: zodResolver(modsSchema),
defaultValues: {
fileName: "",
version: "",
downloadLink: "",
fileSize: ""
}
});
useEffect(() => {
fetchMods();
const interval = setInterval(() => {
fetchMods();
}, 10000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (editMod) {
dialogForm.reset({
fileName: editMod.fileName,
version: editMod.version,
downloadLink: editMod.downloadLink,
fileSize: editMod.fileSize
});
setDialogOpen(true); // Open dialog when a mod is being edited
}
}, [editMod, dialogForm]);
const fetchMods = async () => {
try {
const response = await fetch("/api/mods", {
method: "GET"
method: "GET",
});
if (response.ok) {
const data: ModEntry[] = await response.json();
@ -105,49 +66,38 @@ const SvrjsModsAdminPage = () => {
}
};
useEffect(() => {
fetchMods();
const interval = setInterval(() => {
fetchMods();
}, 10000);
return () => clearInterval(interval);
}, []);
const onSubmit: SubmitHandler<z.infer<typeof modsSchema>> = async (data) => {
setLoading(true);
try {
const response = editMod
? await fetch(`/api/update/mods/${editMod._id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
})
: await fetch("/api/uploadmods", {
const response = await fetch("/api/uploadmods", {
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(data)
body: JSON.stringify(data),
});
if (response.ok) {
mainForm.reset();
dialogForm.reset();
form.reset();
fetchMods();
setLoading(false);
setEditMod(null);
setDialogOpen(false); // Close dialog on successful submission
toast({
description: "Successfully Saved Changes"
description: "Successfully Uploaded Mods",
});
} else {
console.error("Save failed");
console.error("Upload failed");
setLoading(false);
toast({
description: "Save failed",
variant: "destructive"
});
}
} catch (error) {
console.error("Save failed", error);
setLoading(false);
toast({
description: "Save failed",
variant: "destructive"
description: "Upload failed",
variant: "destructive",
});
}
};
@ -155,7 +105,7 @@ const SvrjsModsAdminPage = () => {
const deleteMod = async (id: string) => {
try {
const response = await fetch(`/api/delete/mods/${id}`, {
method: "DELETE"
method: "DELETE",
});
if (response.ok) {
fetchMods();
@ -170,10 +120,10 @@ const SvrjsModsAdminPage = () => {
return (
<section id="mods-page" className="wrapper container">
<h1 className="text-3xl font-bold py-6">Mods Form</h1>
<Form {...mainForm}>
<form onSubmit={mainForm.handleSubmit(onSubmit)} className="space-y-4">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={mainForm.control}
control={form.control}
name="fileName"
render={({ field }) => (
<FormItem>
@ -186,7 +136,7 @@ const SvrjsModsAdminPage = () => {
)}
/>
<FormField
control={mainForm.control}
control={form.control}
name="version"
render={({ field }) => (
<FormItem>
@ -199,7 +149,7 @@ const SvrjsModsAdminPage = () => {
)}
/>
<FormField
control={mainForm.control}
control={form.control}
name="downloadLink"
render={({ field }) => (
<FormItem>
@ -221,7 +171,7 @@ const SvrjsModsAdminPage = () => {
)}
/>
<FormField
control={mainForm.control}
control={form.control}
name="fileSize"
render={({ field }) => (
<FormItem>
@ -239,7 +189,7 @@ const SvrjsModsAdminPage = () => {
size={"lg"}
disabled={loading}
>
{editMod ? "Save Changes" : "Submit"}
Submit
</Button>
</form>
</Form>
@ -261,7 +211,10 @@ const SvrjsModsAdminPage = () => {
</TableRow>
</TableHeader>
<TableBody>
{mods.map((mod) => (
{mods
.slice()
.reverse()
.map((mod) => (
<TableRow key={mod._id}>
<TableCell className="border-b px-4 py-2">
{mod.fileName}
@ -281,106 +234,7 @@ const SvrjsModsAdminPage = () => {
<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>
<TableCell className="border-b px-4 py-2">
<Button
variant={"destructive"}
onClick={() => deleteMod(mod._id)}

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation";
@ -17,7 +17,7 @@ import {
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle
DialogTitle,
} from "@/components/ui/dialog";
interface PageEntry {
@ -47,7 +47,7 @@ const MultiLogs = () => {
const response = await fetch("/api/mdx/pages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: pageTitle, slug, content: "" })
body: JSON.stringify({ title: pageTitle, slug, content: "" }),
});
if (response.ok) {
@ -68,7 +68,7 @@ const MultiLogs = () => {
const deletePage = async (slug: string) => {
setLoading(true);
const response = await fetch(`/api/mdx/pages/${slug}`, {
method: "DELETE"
method: "DELETE",
});
if (response.ok) {
@ -122,7 +122,7 @@ const MultiLogs = () => {
<TableRow key={page.slug}>
<TableCell className="border-b px-4 py-2">
<a
href={`/changelog/${page.slug}`}
href={`/changelogs/${page.slug}`}
className="text-blue-500 underline"
>
{page.slug}

View file

@ -1,6 +1,5 @@
import React from "react";
import Card from "../_components/Card";
import { AdminDashboardLINKS } from "@/constants";
const AdminPage = () => {
return (
@ -9,9 +8,11 @@ const AdminPage = () => {
<h1 className="h2-bold py-6">Admin Page</h1>
<div className="grid lg:grid-cols-2 grid-cols-1 gap-4 ">
{AdminDashboardLINKS.map((item, idx) => (
<Card key={idx} title={item.label} url={item.url} />
))}
<Card title="Downloads" url="/admin/downloads" />
<Card title="Mods" url="/admin/mods" />
<Card title="Logs" url="/admin/changelogs" />
<Card title="MultiLogs" url="/admin/multi-logs" />
<Card title="Vulnerabilities" url="/admin/vulnerabilities" />
</div>
</section>
</>

View file

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

View file

@ -10,7 +10,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import {
Table,
@ -19,54 +19,47 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { logsSchema } from "@/lib/validations/validation";
import { z } from "zod";
import { useToast } from "@/components/ui/use-toast";
import { vulnerabilitiesSchema } from "@/lib/validations/validation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from "@/components/ui/select";
interface VulnerabiltyEntry {
interface LogEntry {
_id: string;
version: string;
bullets: {
point: string;
securityAdvisoryUrl: string;
}[];
date: string;
bullets: { point: string }[];
}
type VulnerabiltiesForm = z.infer<typeof vulnerabilitiesSchema>;
type LogsFormValues = z.infer<typeof logsSchema>;
const AdminLogPage = () => {
const [logs, setLogs] = useState<VulnerabiltyEntry[]>([]);
const [logs, setLogs] = useState<LogEntry[]>([]);
const [error, setError] = useState("");
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const form = useForm<VulnerabiltiesForm>({
resolver: zodResolver(vulnerabilitiesSchema),
const form = useForm<LogsFormValues>({
resolver: zodResolver(logsSchema),
defaultValues: {
version: "",
bullets: [{ point: "", securityAdvisoryUrl: "" }]
}
date: "",
bullets: [{ point: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "bullets"
name: "bullets",
});
const fetchLogs = async () => {
try {
const response = await fetch("/api/vulnerabilities", { method: "GET" });
const response = await fetch("/api/vulnerabilties", { method: "GET" });
if (response.ok) {
const data: VulnerabiltyEntry[] = await response.json();
const data: LogEntry[] = await response.json();
setLogs(data);
} else {
throw new Error(`HTTP error! status: ${response.status}`);
@ -76,12 +69,12 @@ const AdminLogPage = () => {
}
};
const onSubmit: SubmitHandler<VulnerabiltiesForm> = async (data) => {
const onSubmit: SubmitHandler<LogsFormValues> = async (data) => {
setLoading(true);
const response = await fetch("/api/uploadvulnerabilities", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
body: JSON.stringify(data),
});
if (response.ok) {
@ -97,8 +90,8 @@ const AdminLogPage = () => {
const deleteLog = async (id: string) => {
try {
const response = await fetch(`/api/delete/vulnerability/${id}`, {
method: "DELETE"
const response = await fetch(`/api/delete/logs/${id}`, {
method: "DELETE",
});
if (response.ok) {
fetchLogs();
@ -139,7 +132,6 @@ const AdminLogPage = () => {
/>
{fields.map((field, index) => (
<>
<FormField
key={field.id}
control={form.control}
@ -151,22 +143,6 @@ const AdminLogPage = () => {
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
key={field.id + "-securityAdvisory"}
control={form.control}
name={`bullets.${index}.securityAdvisoryUrl`}
render={({ field }) => (
<FormItem>
<FormLabel>
Security Advisory URL for Key Point {index + 1}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<Button
type="button"
className="mt-2"
@ -178,14 +154,13 @@ const AdminLogPage = () => {
</FormItem>
)}
/>
</>
))}
<Button
type="button"
className="mb-4"
size={"icon"}
variant={"outline"}
onClick={() => append({ point: "", securityAdvisoryUrl: "" })}
onClick={() => append({ point: "" })}
>
+
</Button>

View file

@ -1,149 +0,0 @@
"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>
&ldquo;{"{unsubscribeId}"}&rdquo; 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;

View file

@ -1,10 +1,16 @@
import React from "react";
import AuthProvider from "@/components/shared/providers/AuthProvider";
import MobileNav from "./_components/Mobilenav";
import Sidebar from "./_components/Sidebar";
export default function AdminLayout({
children
export default function PageLayout({
children,
}: {
children: React.ReactNode;
}) {
return <AuthProvider>{children}</AuthProvider>;
return (
<main className="flex flex-col min-h-screen root">
<Sidebar />
<MobileNav />
<div className="root-container lg:px-24">{children}</div>
</main>
);
}

View file

@ -1,123 +0,0 @@
/**
* 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;
}

View file

@ -1,98 +0,0 @@
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;
}

View file

@ -1,231 +0,0 @@
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;
}

View file

@ -1,63 +1,11 @@
import React from "react";
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"
}
title: "Blog - SVRJS",
};
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>
);
const BlogPage = () => {
return <div>BlogPage</div>;
};
export default BlogPage;

View file

@ -1,89 +0,0 @@
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;

View file

@ -1,80 +0,0 @@
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;

View file

@ -1,69 +0,0 @@
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;

View file

@ -1,37 +0,0 @@
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;

View file

@ -1,69 +0,0 @@
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;

View file

@ -0,0 +1,87 @@
"use client";
import { Skeleton } from "@/components/ui/skeleton";
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import Head from "next/head";
const Page = ({ params }: { params: { slug: string } }) => {
const { slug } = params;
const [page, setPage] = useState<{ title: string; content: string } | null>(
null
);
const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
useEffect(() => {
const fetchPage = async () => {
try {
const response = await fetch(`/api/mdx/pages/${slug}`);
if (response.ok) {
const data = await response.json();
setPage(data);
return (document.title = `${data.title} | SVRJS`);
} else {
if (response.status === 404) {
setNotFound(true);
return (document.title = "404 Not Found");
}
}
} catch (error) {
console.error("Failed to load page", error);
setNotFound(true);
} finally {
setLoading(false);
}
};
fetchPage();
}, [slug]);
if (loading) {
return (
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
<div className="mb-3">
<Skeleton className="w-[400px] h-[50px] rounded-md" />
</div>
<div className="flex flex-col gap-4">
<Skeleton className="w-[300px] h-[30px] rounded-md" />
<Skeleton className="w-[200px] h-[20px] rounded-md" />
<Skeleton className="w-[200px] h-[20px] rounded-md" />
<Skeleton className="w-[200px] h-[20px] rounded-md" />
</div>
</section>
);
}
if (notFound) {
return (
<section id="404error" className="flex-center flex-col wrapper container">
<h1 className="text-3xl md:text-5xl text-center">
<span className="text-red-500">404</span> Page not Found
</h1>
<p className="text-lg mt-3 text-muted-foreground">
Please return back to Home
</p>
</section>
);
}
if (!page) {
return null;
}
return (
<>
<section className="wrapper container py-24 md:py-28 gap-2 flex flex-col">
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
{page.title}
</h1>
<ReactMarkdown className="prose max-w-full md:prose-lg dark:prose-invert">
{page.content}
</ReactMarkdown>
</section>
</>
);
};
export default Page;

View file

@ -0,0 +1,86 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Download } from "lucide-react";
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import { CHANGE_LOGS } from "@/constants/guidelines";
interface Bullet {
point: string;
}
interface LOGS {
_id: string;
date: string;
version: string;
bullets?: Bullet[]; // Make bullets optional
}
const LogsPage: React.FC = () => {
const [downloads, setDownloads] = useState<LOGS[]>([]);
const [error, setError] = useState("");
const fetchDownloads = async () => {
try {
const response = await fetch("/api/logs", {
method: "GET",
});
if (response.ok) {
const data: LOGS[] = await response.json();
setDownloads(data);
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error: any) {
setError(error.message || "Failed to fetch downloads");
}
};
useEffect(() => {
fetchDownloads();
const interval = setInterval(() => {
fetchDownloads();
}, 10000);
return () => clearInterval(interval);
}, []);
const reversedDownloads = [...downloads].reverse();
return (
<section
id="logs"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
>
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
Server LOGS
</h1>
<p className="md:text-lg text-muted-foreground text-start mb-6">
Get all the latest version of SVRJS download and compiled Files here!
</p>
{error && <p className="text-red-500">{error}</p>}
{reversedDownloads.map((download) => (
<div
key={download._id}
className="flex-start prose max-w-full md:prose-lg dark:prose-invert flex-col mb-4"
>
<h2 className="font-bold text-3xl">{download.version}</h2>
<span className="font-medium italic">{download.date}</span>
<ul className="list-disc pl-5">
{(download.bullets ?? []).map((bullet, index) => (
<li key={index}>{bullet.point}</li>
))}
</ul>
</div>
))}
<div className="prose max-w-full md:prose-lg dark:prose-invert">
<ReactMarkdown>{CHANGE_LOGS}</ReactMarkdown>
</div>
</section>
);
};
export default LogsPage;

View file

@ -1,37 +0,0 @@
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;

View file

@ -12,7 +12,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@ -20,69 +20,55 @@ import { useToast } from "@/components/ui/use-toast";
import { useState } from "react";
import { Separator } from "@/components/ui/separator";
import { emails } from "@/constants";
import HCaptcha from "@hcaptcha/react-hcaptcha";
const ContactUs = () => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [showCaptcha, setShowCaptcha] = useState(false);
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const form = useForm<z.infer<typeof contactFormSchema>>({
resolver: zodResolver(contactFormSchema),
defaultValues: {
name: "",
email: "",
message: ""
}
message: "",
},
});
async function onSubmit(values: z.infer<typeof contactFormSchema>) {
if (!captchaToken) {
setShowCaptcha(true);
return;
}
setLoading(true);
try {
const res = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify({ ...values, captchaToken }),
body: JSON.stringify(values),
headers: {
"Content-Type": "application/json",
Accept: "application/json"
}
Accept: "application/json",
},
});
if (res.ok) {
form.reset();
setCaptchaToken(null); // Reset captcha token after successful submission
toast({
description: "Your message has been sent."
description: "Your message has been sent.",
});
setLoading(false);
} else {
toast({
title: "Uh oh! Something went wrong.",
variant: "destructive"
variant: "destructive",
});
setLoading(false);
}
} catch (error) {
console.error(error);
toast({
title: "Uh oh! Something went wrong.",
variant: "destructive"
variant: "destructive",
});
} finally {
setLoading(false);
setShowCaptcha(false); // Hide captcha after submission attempt
}
}
function handleCaptchaVerify(token: string) {
setCaptchaToken(token);
onSubmit(form.getValues()); // Trigger form submission after captcha is verified
}
return (
<>
<div className="flex items-center justify-center py-12 md:py-16 w-full transition-all duration-300">
@ -142,14 +128,6 @@ const ContactUs = () => {
</FormItem>
)}
/>
{showCaptcha && (
<HCaptcha
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
onVerify={handleCaptchaVerify}
/>
)}
<Button
type="submit"
variant={"default"}

View file

@ -2,41 +2,14 @@ import ReactMarkdown from "react-markdown";
import { contribute } from "@/constants/guidelines";
import { Metadata } from "next";
// baseURL [ENV]
export const metadata: Metadata = {
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"
}
title: "Contribute - SVRJS",
};
const Contribute = () => {
return (
<section
id="tos"
id="contribute"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
>
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">

View file

@ -1,38 +1,11 @@
import { Metadata } from "next";
// baseURL [ENV]
export const metadata: Metadata = {
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"
}
title: "Downloads - SVRJS",
};
export default function DownloadLayout({
children
children,
}: {
children: React.ReactNode;
}) {

View file

@ -1,3 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Table,
@ -6,11 +9,11 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
import { Download } from "lucide-react";
import Link from "next/link";
import clientPromise from "@/lib/db";
import { Skeleton } from "@/components/ui/skeleton";
interface Download {
_id: string;
@ -18,25 +21,38 @@ interface Download {
fileName: string;
version: string;
fileSize: string;
downloadLink?: string; // Optional
downloadLink: string;
}
export const dynamic = "force-static";
const DownloadPage: React.FC = async () => {
let error: Error | null = null;
let downloads: Download[] = [];
const DownloadPage: React.FC = () => {
const [downloads, setDownloads] = useState<Download[]>([]);
const [error, setError] = useState("");
const fetchDownloads = async () => {
try {
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 response = await fetch("/api/downloads", {
method: "GET",
});
if (response.ok) {
const data: Download[] = await response.json();
setDownloads(data);
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error: any) {
setError(error);
}
};
useEffect(() => {
fetchDownloads();
const interval = setInterval(() => {
fetchDownloads();
}, 10000);
return () => clearInterval(interval);
}, []);
return (
<section
@ -47,17 +63,9 @@ const DownloadPage: React.FC = async () => {
Downloads
</h1>
<p className="md:text-lg text-muted-foreground text-start mb-6">
Get all the latest versions of SVR.JS here! Other SVR.JS downloads can
be found in{" "}
<Link
href="https://downloads.svrjs.org"
className="text-black dark:text-white underline"
>
SVR.JS downloads server
</Link>
.
Get all the latest version of SVRJS download and compiled Files here!
</p>
{error && <p className="text-red-500">{error.message}</p>}
{error && <p className="text-red-500">{error}</p>}
<Table>
<TableCaption>A list of all available downloads.</TableCaption>
<TableHeader>
@ -71,7 +79,7 @@ const DownloadPage: React.FC = async () => {
</TableHeader>
<TableBody>
{downloads
.slice(downloads.length - 10 < 0 ? 0 : downloads.length - 10)
.slice(0, 10)
.reverse()
.map((download) => (
<TableRow key={download._id}>
@ -80,19 +88,12 @@ const DownloadPage: React.FC = async () => {
<TableCell>{download.version}</TableCell>
<TableCell className="text-left">{download.fileSize}</TableCell>
<TableCell className="flex items-center justify-end">
{download.downloadLink ? (
<Link href={download.downloadLink}>
<Button variant={"ghost"} className="">
<Download className="w-4 h-4 mr-2" />
Download
</Button>
</Link>
) : (
<Button variant={"ghost"} disabled>
<Download className="w-4 h-4 mr-2" />
Unavailable
</Button>
)}
</TableCell>
</TableRow>
))}

13
app/(root)/forum/page.tsx Normal file
View file

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

View file

@ -1,59 +1,45 @@
import Footer from "@/components/shared/Footer";
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";
// baseURL [ENV]
export const metadata: Metadata = {
title: "SVR.JS - a web server running on Node.JS",
title: "SVRJS - A Web Server running on Node.js",
description:
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.JS. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
"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",
title: "SVRJS - A Web Server running on Node.js",
description:
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.JS. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}`,
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
url: "https://svrjs.org",
type: "website",
images: [
{
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`,
url: "https://svrjs.vercel.app/metadata/svrjs-cover.png",
width: 800,
height: 600,
alt: "SVR.JS - a web server running on Node.JS"
}
]
alt: "SVRJS - 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",
title: "SVRJS - A Web Server running on Node.js",
description:
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.JS. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
images: [`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/svrjs-cover.png`],
creator: "@SVR_JS"
}
"Experience unparalleled flexibility with SVR.JS - the ultimate web server for Node.js. Host web pages, run server-side JavaScript, utilize mods for extended functionality, and more. Integrated log viewer and user management tools included. Also supports Bun (experimental).",
images: ["https://svrjs.vercel.app/metadata/svrjs-cover.png"],
creator: "@SVR_JS",
},
};
export default function PageLayout({
children
children,
}: {
children: React.ReactNode;
}) {
const iconClassName = "w-4 h-4 flex-center text-zinc-950 -mr-2";
return (
<div className="flex flex-col min-h-screen">
{/* Comment or edit this whenever required */}
{/*<Banner
icon={<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>

View file

@ -1,35 +1,9 @@
import { Metadata } from "next";
// baseURL [ENV]
export const metadata: Metadata = {
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"
}
title: "MOD - SVRJS",
};
const ModLayout = ({ children }: { children: React.ReactNode }) => {
return <main>{children}</main>;
};

View file

@ -1,3 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Table,
@ -6,11 +9,11 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
import { Download } from "lucide-react";
import Link from "next/link";
import clientPromise from "@/lib/db";
import { Skeleton } from "@/components/ui/skeleton";
interface Mods {
_id: string;
@ -21,22 +24,35 @@ interface Mods {
downloadLink: string;
}
export const dynamic = "force-static";
const ModsPage: React.FC = async () => {
let error: Error | null = null;
let downloads: Mods[] = [];
const ModsPage: React.FC = () => {
const [downloads, setDownloads] = useState<Mods[]>([]);
const [error, setError] = useState("");
const fetchDownloads = async () => {
try {
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 response = await fetch("/api/mods", {
method: "GET",
});
if (response.ok) {
const data: Mods[] = await response.json();
setDownloads(data);
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error: any) {
setError(error);
}
};
useEffect(() => {
fetchDownloads();
const interval = setInterval(() => {
fetchDownloads();
}, 10000);
return () => clearInterval(interval);
}, []);
return (
<section
@ -44,26 +60,12 @@ const ModsPage: React.FC = async () => {
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 mods
SvrJS Mods
</h1>
<p className="md:text-lg text-muted-foreground text-start mb-6">
Get all the latest version of SVR.JS mods here! Notes can be found at{" "}
<Link
href="/docs/mod-notes"
className="text-black dark:text-white underline"
>
&ldquo;SVR.JS mod notes&rdquo; section in SVR.JS documentation
</Link>
. Other SVR.JS mods downloads can be found in{" "}
<Link
href="https://downloads.svrjs.org/mods"
className="text-black dark:text-white underline"
>
SVR.JS downloads server
</Link>
.
Get all the latest version of SVRJS Mods and compiled Files here!{" "}
</p>
{error && <p className="text-red-500">{error.message}</p>}
{error && <p className="text-red-500">{error}</p>}
<Table>
<TableCaption>A list of all available downloads.</TableCaption>
<TableHeader>
@ -71,12 +73,15 @@ const ModsPage: React.FC = async () => {
<TableHead className="w-[150px]">Date</TableHead>
<TableHead>File Name</TableHead>
<TableHead>Version</TableHead>
<TableHead>File Size</TableHead>
<TableHead className="text-right">Download Link</TableHead>
<TableHead>Download Link</TableHead>
<TableHead className="text-right">File Size</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{downloads.map((download) => (
{downloads
.slice(0, 10)
.reverse()
.map((download) => (
<TableRow key={download._id}>
<TableCell className="font-medium">{download.date}</TableCell>
<TableCell>{download.fileName}</TableCell>

View file

@ -1,39 +0,0 @@
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;

View file

@ -2,19 +2,20 @@ import About from "@/components/shared/About";
import DataTable from "@/components/shared/DataTable";
import Faq from "@/components/shared/FAQ";
import Hero from "@/components/shared/Hero";
import Features from "@/components/shared/Features";
import HowItWorks from "@/components/shared/HowItWorks";
import Newsletter from "@/components/shared/Newsletter";
import DemoVideo from "@/components/shared/DemoVideo";
import Partners from "@/components/shared/Partners";
import Testimonials from "@/components/shared/Testimonials";
const RootPage = () => {
return (
<>
<Hero />
<Features />
<HowItWorks />
<Testimonials />
<DemoVideo />
<Partners />
<About />
{/* <DataTable /> */}
<Faq />
<Newsletter />
</>

View file

@ -0,0 +1,30 @@
import { PRIVACY_POLICY } from "@/constants/guidelines";
import React from "react";
import ReactMarkdown from "react-markdown";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Privacy Policy - SVRJS",
};
const PrivacyPolicy = () => {
return (
<section
id="privacy-policy"
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
>
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
Privacy Policy
</h1>
<p className="md:text-lg text-muted-foreground text-start mb-6">
Effective date: 26.05.2024
</p>
<div className="prose max-w-full md:prose-lg dark:prose-invert">
<ReactMarkdown>{PRIVACY_POLICY}</ReactMarkdown>
</div>
</section>
);
};
export default PrivacyPolicy;

View file

@ -1,57 +0,0 @@
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;

View file

@ -2,35 +2,8 @@ import ReactMarkdown from "react-markdown";
import { TERMS_AND_CONDITIONS } from "@/constants/guidelines";
import { Metadata } from "next";
// baseURL [ENV]
export const metadata: Metadata = {
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"
}
title: "Terms Of Service - SVRJS",
};
const TermsOfService = () => {
@ -40,10 +13,10 @@ const TermsOfService = () => {
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
>
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
Terms of Service
Terms and Conditions
</h1>
<p className="md:text-lg text-muted-foreground text-start mb-6">
Effective date: April 24, 2024
Last updated: 24.04.2024
</p>
<div className="prose max-w-full md:prose-lg dark:prose-invert">
<ReactMarkdown>{TERMS_AND_CONDITIONS}</ReactMarkdown>

View file

@ -1,36 +0,0 @@
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;

View file

@ -1,96 +0,0 @@
"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;

View file

@ -1,37 +0,0 @@
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;

View file

@ -1,60 +1,72 @@
"use client";
import ReactMarkdown from "react-markdown";
import { vulnerabilities } from "@/constants/guidelines";
import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import clientPromise from "@/lib/db";
import Link from "next/link";
interface Bullet {
point: string;
securityAdvisoryUrl: string;
}
interface Vulnerabilities {
_id: string;
date: string;
version: string;
bullets?: Bullet[]; // Make bullets optional
}
interface ModsVulnerability {
_id: string;
title: string;
slug: string;
content: string;
vulnerabilities: string;
}
const Vulnerabilities = async () => {
let downloads: Vulnerabilities[] = [];
let mods: ModsVulnerability[] = [];
let error: Error | null = null;
const Vulnerabilities = () => {
const [loading, setLoading] = useState(true);
const [downloads, setDownloads] = useState<Vulnerabilities[]>([]);
const [error, setError] = useState("");
const fetchData = async () => {
try {
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 response = await fetch("/api/vulnerabilities", {
method: "GET",
});
if (response.ok) {
const data: Vulnerabilities[] = await response.json();
setDownloads(data);
return (document.title = "Vulnerabilities | SVRJS");
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error: any) {
setError(error.message || "Failed to fetch downloads");
} finally {
setLoading(false);
}
};
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
useEffect(() => {
fetchData();
const pages = (await db
.collection("pages")
.find()
.toArray()) as unknown as ModsVulnerability[];
mods = pages.filter(
(mod: ModsVulnerability) =>
mod.vulnerabilities && mod.vulnerabilities.trim() !== ""
);
} catch (err) {}
const interval = setInterval(() => {
fetchData();
}, 10000);
return () => clearInterval(interval);
}, []);
const reversedDownloads = [...downloads].reverse();
const reversedMods = [...mods].reverse();
// initially loading = true
if (loading) {
return (
<section className="wrapper container py-24 md:py-28 gap-4 flex flex-col">
<div className="mb-3">
<Skeleton className="w-[400px] h-[50px] rounded-md" />
</div>
<div className="flex flex-col gap-4">
<Skeleton className="w-[300px] h-[30px] rounded-md" />
<Skeleton className="w-[200px] h-[20px] rounded-md" />
<Skeleton className="w-[200px] h-[20px] rounded-md" />
<Skeleton className="w-[200px] h-[20px] rounded-md" />
</div>
</section>
);
}
return (
<section
@ -62,66 +74,35 @@ const Vulnerabilities = async () => {
className="wrapper container py-24 md:py-28 gap-2 flex flex-col"
>
<h1 className="text-3xl md:text-5xl pb-1 md:pb-2 font-bold text-black dark:bg-clip-text dark:text-transparent dark:bg-gradient-to-b dark:from-white dark:to-neutral-400">
SVR.JS vulnerabilities
SVR.JS Vulnerabilities
</h1>
<p className="md:text-lg text-muted-foreground text-start mb-6">
Some older versions of SVR.JS are vulnerable to cyberattacks. It&apos;s
recommended to update your SVR.JS version to the newest one. If you find
a security issue with SVR.JS, report it as soon as possible to
vulnerability-reports@svrjs.org. We&apos;ll mitigate that vulnerability
if it is possible.
vulnerability-reports[at]svrjs[dot]org. We&apos;ll mitigate that
vulnerability if it is possible.
</p>
{error && <p className="text-red-500">{error.message}</p>}
{error && <p className="text-red-500">{error}</p>}
<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">
SVR.JS
</h2>
{reversedDownloads.map((download) => (
<div
key={download._id}
className="flex-start flex-col prose max-w-full md:prose-lg dark:prose-invert gap-2"
className="flex-start flex-col prose dark:prose-invert mb-4 gap-4"
>
<h3 className="mb-0 md:mb-0">{download.version}</h3>
<ul>
<h2 className="font-semibold text-3xl -mb-2">{download.version}</h2>
<ul className="list-disc pl-5">
{(download.bullets ?? []).map((bullet, index) => (
<li key={index}>
{bullet.point}
{bullet.securityAdvisoryUrl ? (
<>
{" "}
<Link href={bullet.securityAdvisoryUrl} className="italic">
View the security advisory
</Link>
</>
) : (
""
)}
</li>
<li key={index}>{bullet.point}</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>
<ReactMarkdown>{vulnerabilities}</ReactMarkdown>
</div>
)}
</div>
))}
</section>
);
};
export const dynamic = "force-static";
export default Vulnerabilities;

View file

@ -9,7 +9,7 @@ export const authOptions: NextAuthOptions = {
name: "Credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" }
password: { label: "Password", type: "password" },
},
async authorize(credentials: any): Promise<any> {
const adminUsername = process.env.ADMIN_USERNAME;
@ -18,7 +18,7 @@ export const authOptions: NextAuthOptions = {
console.log(adminUsername);
console.log(adminPasswordHash);
console.log(credentials.username);
console.log("[password redacted]");
console.log(credentials.password);
if (credentials.username == adminUsername) {
const isValidPassword = await bcrypt.compare(
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.
return null;
}
})
},
}),
],
callbacks: {
async jwt({ token, user }) {
@ -51,13 +51,13 @@ export const authOptions: NextAuthOptions = {
// session.user.id = token.id;
// session.user.name = token.name;
return session;
}
},
},
pages: {
signIn: "/login"
signIn: "/login",
},
session: {
strategy: "jwt"
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET
secret: process.env.NEXTAUTH_SECRET,
};

View file

@ -1,12 +1,19 @@
import { mailOptions, transporter } from "@/lib/nodemailer/nodemailer";
import { NextRequest, NextResponse } from "next/server";
import dns from "dns/promises";
import { isEmail, escape } from "validator";
const CONTACT_MESSAGE_FIELDS: Record<string, string> = {
name: "Name",
email: "Email",
message: "Message"
message: "Message",
};
const escapeHtml = (text: string) => {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
const generateEmailContent = (data: Record<string, string>) => {
@ -20,14 +27,12 @@ const generateEmailContent = (data: Record<string, string>) => {
const htmlData = Object.entries(data).reduce(
(str, [key, val]) =>
str +
(key == "captchaToken"
? ""
: `<h3 class="form-heading">${escape(
`<h3 class="form-heading">${escapeHtml(
CONTACT_MESSAGE_FIELDS[key] || key
)}</h3><p class="form-answer">${escape(val).replace(
)}</h3><p class="form-answer">${escapeHtml(val).replace(
/\n/g,
"<br/>"
)}</p>`),
)}</p>`,
""
);
@ -85,7 +90,7 @@ const generateEmailContent = (data: Record<string, string>) => {
</tr>
</table>
</body>
</html>`
</html>`,
};
};
@ -101,71 +106,10 @@ export async function POST(req: NextRequest) {
const data = await req.json();
console.log(data);
// 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=${data.captchaToken}`
}
);
const hcaptchaData = await hcaptchaResponse.json();
if (!hcaptchaData.success) {
return NextResponse.json(
{ message: "Captcha verification failed." },
{ status: 400 }
);
}
// 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"
subject: "Contact Email",
});
return NextResponse.json(

View file

@ -2,7 +2,6 @@
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,
@ -12,13 +11,12 @@ export async function DELETE(
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const collection = db.collection("downloads");
const result = await collection.deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 1) {
revalidatePath("/downloads");
return NextResponse.json({ message: "Log deleted successfully" });
} else {
return NextResponse.json({ message: "Log not found" }, { status: 404 });

View file

@ -2,7 +2,6 @@
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,
@ -12,13 +11,12 @@ export async function DELETE(
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const collection = db.collection("logs");
const result = await collection.deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 1) {
revalidatePath("/changelog");
return NextResponse.json({ message: "Log deleted successfully" });
} else {
return NextResponse.json({ message: "Log not found" }, { status: 404 });

View file

@ -2,7 +2,6 @@
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,
@ -12,13 +11,12 @@ export async function DELETE(
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const collection = db.collection("mods");
const result = await collection.deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 1) {
revalidatePath("/mods");
return NextResponse.json({ message: "Log deleted successfully" });
} else {
return NextResponse.json({ message: "Log not found" }, { status: 404 });

View file

@ -1,40 +0,0 @@
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 }
);
}
}

View file

@ -8,7 +8,7 @@ export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const downloads = await db.collection("downloads").find().toArray();
// console.log("Downloads fetched:", downloads);
return NextResponse.json(downloads, { status: 200 });

View file

@ -14,21 +14,21 @@ export async function POST(request: NextRequest) {
const cookie = serialize("auth", "authenticated", {
httpOnly: true,
path: "/",
maxAge: 60 * 60 * 24 // 1 day
maxAge: 60 * 60 * 24, // 1 day
});
return new NextResponse(JSON.stringify({ message: "Login successful" }), {
headers: {
"Set-Cookie": cookie,
"Content-Type": "application/json"
}
"Content-Type": "application/json",
},
});
}
return new NextResponse(JSON.stringify({ message: "Invalid credentials" }), {
status: 401,
headers: {
"Content-Type": "application/json"
}
"Content-Type": "application/json",
},
});
}

View file

@ -8,7 +8,7 @@ export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const downloads = await db.collection("logs").find().toArray();
return NextResponse.json(downloads, { status: 200 });
} catch (error) {

View file

@ -1,13 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/lib/db";
import { revalidatePath } from "next/cache";
export const GET = async (
req: NextRequest,
{ params }: { params: { slug: string } }
) => {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db();
const { slug } = params;
if (!slug) {
@ -28,45 +27,64 @@ export const PUT = async (
{ params }: { params: { slug: string } }
) => {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db();
const { slug } = params;
if (!slug) {
return NextResponse.json({ message: "Slug is required" }, { status: 400 });
}
const { title, content, vulnerabilities } = await req.json();
const { title, content } = await req.json();
if (
typeof title !== "string" ||
typeof content !== "string" ||
typeof vulnerabilities !== "string"
) {
if (typeof title !== "string" || typeof content !== "string") {
return NextResponse.json(
{ message: "Invalid title, content, or vulnerabilities" },
{ message: "Invalid title or content" },
{ status: 400 }
);
}
try {
const result = await db.collection("pages").findOneAndUpdate(
// it works here ig
const result = await db
.collection("pages")
.findOneAndUpdate(
{ slug },
{ $set: { title, content, vulnerabilities } },
{ returnDocument: "after" } // Updated option
{ $set: { title, content } },
{ returnDocument: "after" }
);
if (result?.value) {
// i hate my life fr fr
console.log("Update Result:", result);
// result returns like
// Update Result: {
// _id: new ObjectId('66a2946b2b91eef505eef943'),
// title: 'TEST PAGE',
// slug: 'test-page',
// content: 'asd]---\n' +
// '---\n' +
// '\n' +
// 'this is basic heading ?\n' +
// '\n' +
// '**HELLO**\n' +
// '\n' +
// 'erw\n' +
// '\n' +
// 'trying another time for test'
// }
// ERRROR : TypeError: Cannot read properties of undefined (reading '_id')
// aposdjaoi sdio JUST WORK NIAWWWWWWWWW
// if (result && result.value) {
const serializedResult = {
...result.value,
_id: result.value._id.toString() // Convert ObjectId to string
...result?.value,
_id: result?.value._id.toString(), // Convert ObjectId to string
};
revalidatePath(`/changelog/${slug}`);
revalidatePath("/vulnerabilities");
revalidatePath("/sitemap.xml");
return NextResponse.json(serializedResult, { status: 200 });
} else {
return NextResponse.json({ message: "Page not found" }, { status: 404 });
}
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(
@ -81,7 +99,7 @@ export const DELETE = async (
{ params }: { params: { slug: string } }
) => {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db();
const { slug } = params;
if (!slug) {
@ -92,9 +110,6 @@ export const DELETE = async (
const result = await db.collection("pages").deleteOne({ slug });
if (result.deletedCount > 0) {
revalidatePath(`/changelog/${slug}`);
revalidatePath("/vulnerabilities");
revalidatePath("/sitemap.xml");
return NextResponse.json(
{ message: "Page deleted successfully" },
{ status: 200 }

View file

@ -1,10 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/lib/db";
import { revalidatePath } from "next/cache";
export const GET = async (req: NextRequest) => {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db();
try {
const pages = await db.collection("pages").find().toArray();
@ -20,7 +19,7 @@ export const GET = async (req: NextRequest) => {
export const POST = async (req: NextRequest) => {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db();
const { title, slug, content } = await req.json();
if (!slug) {
@ -33,9 +32,6 @@ export const POST = async (req: NextRequest) => {
try {
const newPage = { title, slug, content };
const result = await db.collection("pages").insertOne(newPage);
revalidatePath(`/changelog/${slug}`);
revalidatePath("/vulnerabilities");
revalidatePath("/sitemap.xml");
return NextResponse.json(newPage, { status: 201 });
} catch (error) {
console.error("Error creating page:", error);

View file

@ -8,7 +8,7 @@ export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const downloads = await db.collection("mods").find().toArray();
return NextResponse.json(downloads, { status: 200 });
} catch (error) {

View file

@ -1,64 +0,0 @@
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();
}
}

View file

@ -1,44 +0,0 @@
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 });
}
}

View file

@ -1,52 +0,0 @@
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();
}
}

View file

@ -1,80 +0,0 @@
/**
* 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 }
);
}
}

View file

@ -1,134 +0,0 @@
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 }
);
}
}

View file

@ -1,55 +0,0 @@
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 }
);
}
}

View file

@ -1,49 +0,0 @@
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"
});
}
}

View file

@ -1,6 +1,5 @@
import { NextResponse } from "next/server";
import clientPromise from "@/lib/db";
import { revalidatePath } from "next/cache";
// Force the API to use SSR instead of static generation
export const dynamic = "force-dynamic";
@ -10,17 +9,15 @@ export async function POST(request: Request) {
const { fileName, version, downloadLink, fileSize } = body;
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const result = await db.collection("downloads").insertOne({
date: new Date().toISOString().split("T")[0],
fileName,
version,
downloadLink,
fileSize
fileSize,
});
revalidatePath("/downloads");
return NextResponse.json({ success: true, id: result.insertedId });
}

View file

@ -1,6 +1,5 @@
import { NextResponse } from "next/server";
import clientPromise from "@/lib/db";
import { revalidatePath } from "next/cache";
// Force the API to use SSR instead of static generation
export const dynamic = "force-dynamic";
@ -10,15 +9,13 @@ export async function POST(request: Request) {
const { version, date, bullets } = body;
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const result = await db.collection("logs").insertOne({
version,
date,
bullets
bullets,
});
revalidatePath("/changelog");
return NextResponse.json({ success: true, id: result.insertedId });
}

View file

@ -1,6 +1,5 @@
import { NextResponse } from "next/server";
import clientPromise from "@/lib/db";
import { revalidatePath } from "next/cache";
// Force the API to use SSR instead of static generation
export const dynamic = "force-dynamic";
@ -10,17 +9,15 @@ export async function POST(request: Request) {
const { fileName, version, downloadLink, fileSize } = body;
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const result = await db.collection("mods").insertOne({
date: new Date().toISOString().split("T")[0],
fileName,
version,
downloadLink,
fileSize
fileSize,
});
revalidatePath("/mods");
return NextResponse.json({ success: true, id: result.insertedId });
}

View file

@ -6,11 +6,17 @@ const f = createUploadthing();
// const auth = (req: Request) => ({ id: "fakeId" });
export const ourFileRouter = {
imageUploader: f({
"application/zip": { maxFileSize: "8MB" }
}).onUploadComplete(async ({ metadata, file }) => {
imageUploader: f({ "application/zip": { maxFileSize: "8MB" } })
// .middleware(async ({ req }) => {
// const user = await auth(req);
// if (!user) throw new UploadThingError("Unauthorized");
// return { userId: user.id };
// })
.onUploadComplete(async ({ metadata, file }) => {
// console.log("Upload complete for userId:", metadata.userId);
console.log("file url", file.url);
})
// return { uploadedBy: metadata.userId };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;

View file

@ -1,8 +1,9 @@
import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";
// Force the API to use SSR instead of static generation
export const dynamic = "force-dynamic";
export const { GET, POST } = createRouteHandler({
router: ourFileRouter
router: ourFileRouter,
});

View file

@ -1,6 +1,5 @@
import { NextResponse } from "next/server";
import clientPromise from "@/lib/db";
import { revalidatePath } from "next/cache";
// Force the API to use SSR instead of static generation
export const dynamic = "force-dynamic";
@ -10,14 +9,12 @@ export async function POST(request: Request) {
const { version, date, bullets } = body;
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const result = await db.collection("vulnerabilities").insertOne({
version,
bullets
bullets,
});
revalidatePath("/vulnerabilities");
return NextResponse.json({ success: true, id: result.insertedId });
}

View file

@ -8,7 +8,7 @@ export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
try {
const client = await clientPromise;
const db = client.db(process.env.MONGODB_DB);
const db = client.db("downloadsDatabase");
const downloads = await db.collection("vulnerabilities").find().toArray();
return NextResponse.json(downloads, { status: 200 });
} catch (error) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -2,60 +2,37 @@ import type { Metadata } from "next";
import { Poppins } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/shared/providers/themeprovider";
import AuthProvider from "@/components/shared/providers/AuthProvider";
import { Toaster } from "@/components/ui/toaster";
import Analytics from "@/components/shared/providers/Analytics";
const poppins = Poppins({
weight: ["400", "600", "700", "900"],
subsets: ["latin"]
subsets: ["latin"],
});
export const metadata: Metadata = {
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).",
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"
}
title: "SVRJS - A Web Server running on Nodejs",
description: "Open Source Software Library",
};
export default function RootLayout({
children
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en-US" suppressHydrationWarning>
<html lang="en" suppressHydrationWarning>
<body className={`antialiased ${poppins.className}`}>
<ThemeProvider
attribute="class"
enableSystem={true}
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
<AuthProvider>
{children}
<Toaster />
<Analytics pagesRouter={false} />
</AuthProvider>
</ThemeProvider>
</body>
</html>

View file

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

View file

@ -30,7 +30,7 @@ const LoginPage = () => {
const res = await signIn("credentials", {
redirect: false,
username,
password
password,
});
if (res?.ok) {
@ -42,7 +42,7 @@ const LoginPage = () => {
return (
<div className="max-w-xl h-screen flex justify-center bg-gray-900 flex-col container">
<h1 className="text-3xl font-bold mb-4">SVR.JS ADMIN PANEL</h1>
<h1 className="text-3xl font-bold mb-4">SVRJS ADMIN PANEL</h1>
{error && <p className="text-red-500 mb-4">{error}</p>}
<form onSubmit={handleLogin}>
<div className="mb-4">

View file

@ -1,29 +1,8 @@
import type { Metadata } from "next";
import Footer from "@/components/shared/Footer";
import Navbar from "@/components/shared/Navbar";
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 = () => {
return (
<>
<div className="flex flex-col min-h-screen">
<Navbar />
<NoScript />
<section
id="404error"
className="flex-center flex-col wrapper container flex-1 flex-grow"
>
<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>
@ -34,9 +13,6 @@ const NotFound = () => {
</Link>
</p>
</section>
<Footer />
</div>
</>
);
};

View file

@ -1,12 +0,0 @@
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`
};
}

View file

@ -1,49 +0,0 @@
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"
}
});
}

View file

@ -1,73 +1,20 @@
import { getAllBlogPostSlugs } from "@/lib/getBlogPost";
import clientPromise from "@/lib/db";
export default async function sitemap() {
const blogPostSlugs = await getAllBlogPostSlugs();
const baseRoutes = [
"/",
let routes = [
"",
"/blog",
"/changelog",
"/changelogs",
"/contact",
"/contribute",
"/downloads",
"/forum",
"/mods",
"/privacy",
"/privacy-policy",
"/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]
url: `https://vimfn.in${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];
return [...routes];
}

View file

@ -1,11 +0,0 @@
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 Normal file

Binary file not shown.

View file

@ -1,3 +0,0 @@
module.exports = {
extends: ["@commitlint/config-conventional"]
};

View file

@ -1,159 +0,0 @@
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;

View file

@ -1,30 +0,0 @@
"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;

View file

@ -14,7 +14,7 @@ const TestimonialCard = ({
name,
role,
testimonial,
rating
rating,
}: TestimonialCard) => {
return (
<li className="inline-block w-full">

View file

@ -1,26 +0,0 @@
"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;
}

View file

@ -8,11 +8,11 @@ const About = () => {
<div className="bg-accent/50 border rounded-lg py-12">
<div className="px-6 flex flex-col-reverse md:flex-row gap-8 md:gap-12">
<Image
src="/logo.svg"
alt="SVR.JS logo"
width={172}
height={172}
className="w-[172px] object-contain rounded-lg flex-shrink-0 mx-auto md:pl-6"
src="/about.svg"
alt="aboutpicture"
width={300}
height={300}
className="w-[300px] object-contain rounded-lg flex-shrink-0"
/>
<div className="flex flex-col justify-between">
@ -20,16 +20,15 @@ const About = () => {
<h2 className="text-3xl md:text-5xl font-bold">
About{" "}
<span className="bg-gradient-to-b from-green-300 to-primary text-transparent bg-clip-text">
SVR.JS
SVRJS!
</span>
</h2>
<p className="text-lg text-muted-foreground mt-4">
SVR.JS is a web server that runs on top of Node.JS, enabling
server-side JavaScript on webpages. SVR.JS also has an
integrated log viewer, log highlighter, and user management
tool. You can host a webpage using SVR.JS, run server-side
JavaScript, use mods to expand server functionality, or use it
as a forward or reverse proxy.
Host a webpage, run server-side JavaScript, use mods to expand
server functionality, or use it as a forward or reverse proxy.
SVRJS is a web server that runs on top of Node.JS, enabling
server-side JS on webpages. SVRJS also has an integrated log
viewer, log highlighter, and user management tool.
</p>
</div>
<Statistics />

View file

@ -1,50 +0,0 @@
"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