added vercel analytics / speed insights and compliance
This commit is contained in:
parent
02d686ccae
commit
857003c77b
10 changed files with 311 additions and 11 deletions
6
bun.lock
6
bun.lock
|
|
@ -5,6 +5,8 @@
|
|||
"name": "web",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.0.0",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"@vercel/speed-insights": "^1.3.1",
|
||||
"@xboxreplay/xboxlive-api": "^3.4.3",
|
||||
"@yudiel/react-qr-scanner": "^2.5.0",
|
||||
"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=="],
|
||||
|
||||
"@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/xboxlive-api": ["@xboxreplay/xboxlive-api@3.4.3", "", { "dependencies": { "@xboxreplay/errors": "^0.1.0", "axios": "^0.21.1" } }, "sha512-+iSmNCN4RHXZXBc48e0tJB+tvUs6XEwEWcXTcECrIttgDVN9fsFxroo+6ET+K38FobEM+5xUOfCVNqQoWQthxQ=="],
|
||||
|
|
|
|||
|
|
@ -4,12 +4,17 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"prebuild": "node scripts/update-last-modified.js",
|
||||
"build": "next build",
|
||||
"postinstall": "node scripts/update-last-modified.js",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"update-last-modified": "node scripts/update-last-modified.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.0.0",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"@vercel/speed-insights": "^1.3.1",
|
||||
"@xboxreplay/xboxlive-api": "^3.4.3",
|
||||
"@yudiel/react-qr-scanner": "^2.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
|
|||
89
scripts/update-last-modified.js
Normal file
89
scripts/update-last-modified.js
Normal 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();
|
||||
|
|
@ -47,6 +47,13 @@
|
|||
--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 {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import "./globals.css";
|
|||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
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 = {
|
||||
title: "FRG Network",
|
||||
description: "Authenticate to access Minecraft.",
|
||||
|
|
@ -34,8 +37,10 @@ export default function RootLayout({
|
|||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
>
|
||||
{children}
|
||||
<CookieConsent />
|
||||
<ConditionalAnalytics />
|
||||
</ThemeProvider>
|
||||
<Toaster position="bottom-center" />
|
||||
</body>
|
||||
|
|
|
|||
104
src/app/privacy/page.tsx
Normal file
104
src/app/privacy/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
src/components/conditional-analytics.tsx
Normal file
24
src/components/conditional-analytics.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
42
src/components/cookie-notice.tsx
Normal file
42
src/components/cookie-notice.tsx
Normal 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;
|
||||
}
|
||||
|
|
@ -17,14 +17,14 @@ const buttonVariants = cva(
|
|||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"icon-xs": "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5",
|
||||
"icon-sm": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-lg": "size-8 [&_svg:not([class*='size-'])]:size-4",
|
||||
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 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 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 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 py-3 sm:py-2 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
"icon-xs": "size-5 rounded-sm py-2 sm:py-1 [&_svg:not([class*='size-'])]:size-2.5",
|
||||
"icon-sm": "size-6 py-2 sm:py-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-lg": "size-8 py-3 sm:py-2 [&_svg:not([class*='size-'])]:size-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
18
src/lib/git/getLastCommitDate.ts
Normal file
18
src/lib/git/getLastCommitDate.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue