From 857003c77b480f016a856c1575aa8ff5183a6511 Mon Sep 17 00:00:00 2001 From: anonymousratwastaken <108170934+anonymousratwastaken@users.noreply.github.com> Date: Sun, 21 Dec 2025 08:51:14 +0000 Subject: [PATCH] added vercel analytics / speed insights and compliance --- bun.lock | 6 ++ package.json | 7 +- scripts/update-last-modified.js | 89 +++++++++++++++++++ src/app/globals.css | 7 ++ src/app/layout.tsx | 9 +- src/app/privacy/page.tsx | 104 +++++++++++++++++++++++ src/components/conditional-analytics.tsx | 24 ++++++ src/components/cookie-notice.tsx | 42 +++++++++ src/components/ui/button.tsx | 16 ++-- src/lib/git/getLastCommitDate.ts | 18 ++++ 10 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 scripts/update-last-modified.js create mode 100644 src/app/privacy/page.tsx create mode 100644 src/components/conditional-analytics.tsx create mode 100644 src/components/cookie-notice.tsx create mode 100644 src/lib/git/getLastCommitDate.ts diff --git a/bun.lock b/bun.lock index b2a1fea..af44456 100644 --- a/bun.lock +++ b/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=="], diff --git a/package.json b/package.json index 7a280a6..b27fbda 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/update-last-modified.js b/scripts/update-last-modified.js new file mode 100644 index 0000000..126a7be --- /dev/null +++ b/scripts/update-last-modified.js @@ -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*\*\/\}/, + `

+ ${lastUpdatedText} +

` + ); + + 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(); diff --git a/src/app/globals.css b/src/app/globals.css index 241a574..4e1a648 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3cc8739..87f6d14 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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} + + diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx new file mode 100644 index 0000000..a17434d --- /dev/null +++ b/src/app/privacy/page.tsx @@ -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 ( +
+ +

Privacy Policy

+

+ Last updated: December 21, 2025 +

+

FRG Network is a private Minecraft server.

+
+
+

What we collect

+

+ 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: +

+
    +
  • page views and navigation patterns
  • +
  • device type, browser, and operating system
  • +
  • approximate location (country level only)
  • +
  • performance metrics like page load times
  • +
+
+ +
+

How we use this data

+

We use this information to:

+
    +
  • improve website performance and user experience
  • +
  • understand how visitors use our site
  • +
  • optimise our content and features
  • +
+
+
+

What we don't collect

+

We don't collect:

+
    +
  • personal identifiable information
  • +
  • IP addresses
  • +
  • Minecraft account details or passwords
  • +
  • precise location data
  • +
+
+
+

Third party services

+

+ We use Vercel Analytics and Speed Insights. You can read their privacy policy {" "} + + here + . +

+
+
+

Your choices and rights

+
+

Analytics are entirely optional and this website works without them. You may manage your preferences below:

+ +
+ + +
+ +

+ 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. +

+
+
+
+
+ ); +} diff --git a/src/components/conditional-analytics.tsx b/src/components/conditional-analytics.tsx new file mode 100644 index 0000000..66d1cb9 --- /dev/null +++ b/src/components/conditional-analytics.tsx @@ -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 ( + <> + + + + ); +} \ No newline at end of file diff --git a/src/components/cookie-notice.tsx b/src/components/cookie-notice.tsx new file mode 100644 index 0000000..be87771 --- /dev/null +++ b/src/components/cookie-notice.tsx @@ -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.{" "} + + Privacy Policy + + + ), + 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; +} \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index d26a4e3..0ab0ab8 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -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: { diff --git a/src/lib/git/getLastCommitDate.ts b/src/lib/git/getLastCommitDate.ts new file mode 100644 index 0000000..b4c51e4 --- /dev/null +++ b/src/lib/git/getLastCommitDate.ts @@ -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; + } +}