added vercel analytics / speed insights and compliance

This commit is contained in:
anonymousratwastaken 2025-12-21 08:51:14 +00:00
parent 02d686ccae
commit 857003c77b
10 changed files with 311 additions and 11 deletions

View file

@ -5,6 +5,8 @@
"name": "web", "name": "web",
"dependencies": { "dependencies": {
"@base-ui/react": "^1.0.0", "@base-ui/react": "^1.0.0",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"@xboxreplay/xboxlive-api": "^3.4.3", "@xboxreplay/xboxlive-api": "^3.4.3",
"@yudiel/react-qr-scanner": "^2.5.0", "@yudiel/react-qr-scanner": "^2.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -507,6 +509,10 @@
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
"@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="],
"@vercel/speed-insights": ["@vercel/speed-insights@1.3.1", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ=="],
"@xboxreplay/errors": ["@xboxreplay/errors@0.1.0", "", {}, "sha512-Tgz1d/OIPDWPeyOvuL5+aai5VCcqObhPnlI3skQuf80GVF3k1I0lPCnGC+8Cm5PV9aLBT5m8qPcJoIUQ2U4y9g=="], "@xboxreplay/errors": ["@xboxreplay/errors@0.1.0", "", {}, "sha512-Tgz1d/OIPDWPeyOvuL5+aai5VCcqObhPnlI3skQuf80GVF3k1I0lPCnGC+8Cm5PV9aLBT5m8qPcJoIUQ2U4y9g=="],
"@xboxreplay/xboxlive-api": ["@xboxreplay/xboxlive-api@3.4.3", "", { "dependencies": { "@xboxreplay/errors": "^0.1.0", "axios": "^0.21.1" } }, "sha512-+iSmNCN4RHXZXBc48e0tJB+tvUs6XEwEWcXTcECrIttgDVN9fsFxroo+6ET+K38FobEM+5xUOfCVNqQoWQthxQ=="], "@xboxreplay/xboxlive-api": ["@xboxreplay/xboxlive-api@3.4.3", "", { "dependencies": { "@xboxreplay/errors": "^0.1.0", "axios": "^0.21.1" } }, "sha512-+iSmNCN4RHXZXBc48e0tJB+tvUs6XEwEWcXTcECrIttgDVN9fsFxroo+6ET+K38FobEM+5xUOfCVNqQoWQthxQ=="],

View file

@ -4,12 +4,17 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"prebuild": "node scripts/update-last-modified.js",
"build": "next build", "build": "next build",
"postinstall": "node scripts/update-last-modified.js",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint",
"update-last-modified": "node scripts/update-last-modified.js"
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.0.0", "@base-ui/react": "^1.0.0",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"@xboxreplay/xboxlive-api": "^3.4.3", "@xboxreplay/xboxlive-api": "^3.4.3",
"@yudiel/react-qr-scanner": "^2.5.0", "@yudiel/react-qr-scanner": "^2.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View file

@ -0,0 +1,89 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Get the root directory of the git repository
function getGitRoot() {
try {
return execSync('git rev-parse --show-toplevel').toString().trim();
} catch (error) {
console.error('Error finding git root:', error.message);
return null;
}
}
function getLastCommitDate(filePath) {
try {
const gitRoot = getGitRoot();
if (!gitRoot) {
console.error('Could not find git repository root');
return null;
}
// Get the relative path from git root
const relativePath = path.relative(gitRoot, path.resolve(process.cwd(), filePath));
console.log(`Checking git history for: ${relativePath}`);
// Check if the file is tracked by git
const isTracked = execSync(`git ls-files --error-unmatch "${relativePath}" 2>/dev/null || echo ""`).toString().trim();
if (!isTracked) {
console.log('File is not tracked by git, using current date');
return new Date().toISOString();
}
// Get the last commit date for the file
const lastCommitDate = execSync(
`git log -1 --format="%ad" --date=iso -- "${relativePath}"`,
{ cwd: gitRoot }
).toString().trim();
return lastCommitDate || new Date().toISOString();
} catch (error) {
console.error('Error getting last commit date:', error.message);
return new Date().toISOString();
}
}
function updateLastModified() {
const privacyPagePath = path.join(process.cwd(), 'src/app/privacy/page.tsx');
try {
let content = fs.readFileSync(privacyPagePath, 'utf8');
const lastCommitDate = getLastCommitDate(privacyPagePath);
if (!lastCommitDate) {
console.log('Using current date as fallback');
return;
}
const formattedDate = new Date(lastCommitDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const lastUpdatedText = `Last updated: ${formattedDate}`;
// Check if we need to add or update the last updated line
if (content.includes('LAST_UPDATED_PLACEHOLDER')) {
// Replace the placeholder with the actual date
const updatedContent = content.replace(
/\{\/\*\s*LAST_UPDATED_PLACEHOLDER\s*\*\/\}/,
`<p className="text-sm text-muted-foreground mb-4">
${lastUpdatedText}
</p>`
);
if (updatedContent !== content) {
fs.writeFileSync(privacyPagePath, updatedContent, 'utf8');
console.log('✅ Updated last modified date in privacy policy');
}
} else {
console.log('ℹ️ Last modified date is up to date');
}
} catch (error) {
console.error('Error updating last modified date:', error.message);
}
}
updateLastModified();

View file

@ -47,6 +47,13 @@
--radius-4xl: calc(var(--radius) + 16px); --radius-4xl: calc(var(--radius) + 16px);
} }
[data-sonner-toaster][data-sonner-theme="dark"] [data-sonner-toast][data-styled="true"] [data-cancel] {
@apply rounded-sm border-border dark:bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground;
}
[data-sonner-toaster][data-sonner-theme="dark"] [data-sonner-toast][data-styled="true"] [data-action] {
@apply rounded-sm bg-primary text-primary-foreground hover:bg-primary/80;
}
:root { :root {
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);

View file

@ -3,7 +3,10 @@ import "./globals.css";
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { Google_Sans_Code, Mona_Sans } from "next/font/google"; import { Google_Sans_Code, Mona_Sans } from "next/font/google";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/next"
import { CookieConsent } from "@/components/cookie-notice";
import { ConditionalAnalytics } from "@/components/conditional-analytics";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "FRG Network", title: "FRG Network",
description: "Authenticate to access Minecraft.", description: "Authenticate to access Minecraft.",
@ -36,6 +39,8 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
{children} {children}
<CookieConsent />
<ConditionalAnalytics />
</ThemeProvider> </ThemeProvider>
<Toaster position="bottom-center" /> <Toaster position="bottom-center" />
</body> </body>

104
src/app/privacy/page.tsx Normal file
View file

@ -0,0 +1,104 @@
"use client"
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
export default function PrivacyPage() {
return (
<div className="min-h-screen max-w-3xl mx-auto p-8 flex items-start gap-3 flex-col">
<Button variant="ghost" onClick={() => window.history.back()}>
<ArrowLeft className="mr-2 h-4 w-4" /> Back to action
</Button>
<h1 className="text-3xl text-primary font-bold mb-1">Privacy Policy</h1>
<p className="text-sm text-muted-foreground mb-4">
Last updated: December 21, 2025
</p>
<p>FRG Network is a private Minecraft server.</p>
<div className="text-sm space-y-6">
<section className="space-y-3">
<h2 className="text-xl font-semibold mb-2">What we collect</h2>
<p>
If you choose to accept our cookie policy, FRG Network use Vercel
Analytics and Speed Insights to analyse how people use our website
and how we can improve our services. This monitors:
</p>
<ul className="list-disc list-inside mt-2 space-y-1 ml-4">
<li>page views and navigation patterns</li>
<li>device type, browser, and operating system</li>
<li>approximate location (country level only)</li>
<li>performance metrics like page load times</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-xl font-semibold mb-2">How we use this data</h2>
<p>We use this information to:</p>
<ul className="list-disc list-inside mt-2 space-y-1 ml-4">
<li>improve website performance and user experience</li>
<li>understand how visitors use our site</li>
<li>optimise our content and features</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-xl font-semibold mb-2">What we don't collect</h2>
<p>We don't collect:</p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li>personal identifiable information</li>
<li>IP addresses</li>
<li>Minecraft account details or passwords</li>
<li>precise location data</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-xl font-semibold mb-2">Third party services</h2>
<p>
We use Vercel Analytics and Speed Insights. You can read their privacy policy {" "}
<Link
href="https://vercel.com/legal/privacy-policy"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline-1 underline decoration-1"
>
here
</Link>.
</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-semibold mb-2">Your choices and rights</h2>
<div className="space-y-4">
<p>Analytics are entirely optional and this website works without them. You may manage your preferences below:</p>
<div className="flex flex-col sm:flex-row gap-4 items-start">
<Button
variant="outline"
onClick={() => {
localStorage.setItem('analytics-enabled', 'false');
localStorage.setItem('cookie-consent', 'declined');
window.location.reload();
}}
className="w-full sm:w-auto"
>
Disable Analytics
</Button>
<Button
variant="outline"
onClick={() => {
localStorage.clear();
window.location.reload();
}}
className="w-full sm:w-auto"
>
Reset All Preferences
</Button>
</div>
<p className="text-sm text-muted-foreground">
You can also clear your browser's cache at any time to reset all preferences.
This will not affect the functionality of the website in any way.
</p>
</div>
</section>
</div>
</div>
);
}

View file

@ -0,0 +1,24 @@
"use client";
import { useEffect, useState } from "react";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
export function ConditionalAnalytics() {
const [analyticsEnabled, setAnalyticsEnabled] = useState(false);
useEffect(() => {
// check if analytics are enabled
const enabled = localStorage.getItem("analytics-enabled") === "true";
setAnalyticsEnabled(enabled);
}, []);
if (!analyticsEnabled) return null;
return (
<>
<Analytics />
<SpeedInsights />
</>
);
}

View file

@ -0,0 +1,42 @@
"use client";
import { useEffect } from "react";
import { toast } from "sonner";
export function CookieConsent() {
useEffect(() => {
const consent = localStorage.getItem("cookie-consent");
if (!consent) {
setTimeout(() => {
toast("Cookies & Analytics", {
description: (
<>
We use Vercel Analytics to get useful info on how people use this site. Analytics are entirely anonymous and do not track individual users.{" "}
<a href="/privacy" className="underline hover:text-primary">
Privacy Policy
</a>
</>
),
duration: Infinity,
action: {
label: "Accept",
onClick: () => {
localStorage.setItem("cookie-consent", "accepted");
localStorage.setItem("analytics-enabled", "true");
window.location.reload();
},
},
cancel: {
label: "Opt out",
onClick: () => {
localStorage.setItem("cookie-consent", "declined");
localStorage.setItem("analytics-enabled", "false");
},
},
});
}, 1000);
}
}, []);
return null;
}

View file

@ -17,14 +17,14 @@ const buttonVariants = cva(
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", default: "h-7 gap-1 px-2 py-3 sm:py-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5", xs: "h-5 gap-1 rounded-sm px-2 py-2 sm:py-1 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5",
sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-6 gap-1 px-2 py-2 sm:py-1.5 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4", lg: "h-8 gap-1 px-2.5 py-3 sm:py-2 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4",
icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5", icon: "size-7 py-3 sm:py-2 [&_svg:not([class*='size-'])]:size-3.5",
"icon-xs": "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5", "icon-xs": "size-5 rounded-sm py-2 sm:py-1 [&_svg:not([class*='size-'])]:size-2.5",
"icon-sm": "size-6 [&_svg:not([class*='size-'])]:size-3", "icon-sm": "size-6 py-2 sm:py-1.5 [&_svg:not([class*='size-'])]:size-3",
"icon-lg": "size-8 [&_svg:not([class*='size-'])]:size-4", "icon-lg": "size-8 py-3 sm:py-2 [&_svg:not([class*='size-'])]:size-4",
}, },
}, },
defaultVariants: { defaultVariants: {

View file

@ -0,0 +1,18 @@
import { execSync } from 'child_process';
import path from 'path';
export function getLastCommitDate(filePath: string): string | null {
try {
const absolutePath = path.resolve(process.cwd(), filePath);
const relativePath = path.relative(process.cwd(), absolutePath);
const lastCommitDate = execSync(
`git log -1 --format="%ad" --date=iso -- "${relativePath}"`
).toString().trim();
return lastCommitDate || null;
} catch (error) {
console.error('Error getting last commit date:', error);
return null;
}
}