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",
|
"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=="],
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
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);
|
--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);
|
||||||
|
|
|
||||||
|
|
@ -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
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",
|
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: {
|
||||||
|
|
|
||||||
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