mirror of
https://github.com/PlatypusPus/MushroomEmpire.git
synced 2026-02-07 22:18:59 +00:00
frontend
This commit is contained in:
18
frontend/nordic-privacy-ai/.gitignore
vendored
Normal file
18
frontend/nordic-privacy-ai/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# OS / Editor
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
28
frontend/nordic-privacy-ai/README.md
Normal file
28
frontend/nordic-privacy-ai/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Nordic Privacy AI
|
||||||
|
|
||||||
|
AI-Powered GDPR compliance & personal data protection platform tailored for Nordic ecosystems (BankID, MitID, Suomi.fi).
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- Next.js (App Router, TypeScript)
|
||||||
|
- Tailwind CSS
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
Visit http://localhost:3000 to view the landing page.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
- `npm run dev` – Start dev server
|
||||||
|
- `npm run build` – Production build
|
||||||
|
- `npm start` – Run built app
|
||||||
|
- `npm run lint` – ESLint
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
- Implement /try page workflow
|
||||||
|
- Add feature sections & agent explanations
|
||||||
|
- Integrate backend services for data upload & scanning
|
||||||
|
|
||||||
|
## License
|
||||||
|
Internal hackathon prototype
|
||||||
46
frontend/nordic-privacy-ai/app/globals.css
Normal file
46
frontend/nordic-privacy-ai/app/globals.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }
|
||||||
|
|
||||||
|
/* Custom utilities */
|
||||||
|
.container-max { @apply max-w-7xl mx-auto px-4; }
|
||||||
|
|
||||||
|
/* Smooth scrolling for in-page anchors */
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
|
/* Subtle hero background animations */
|
||||||
|
@keyframes drift {
|
||||||
|
0% { transform: translate3d(0,0,0) scale(1) rotate(0deg); }
|
||||||
|
50% { transform: translate3d(10px,-8px,0) scale(1.02) rotate(8deg); }
|
||||||
|
100% { transform: translate3d(0,0,0) scale(1) rotate(0deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-drift-slow {
|
||||||
|
animation: drift 16s ease-in-out infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-drift-slower {
|
||||||
|
animation: drift 22s ease-in-out infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reveal on scroll */
|
||||||
|
.reveal { opacity: 0; transform: translateY(16px); transition: opacity 500ms ease, transform 600ms ease; }
|
||||||
|
.reveal-visible { opacity: 1; transform: translateY(0); }
|
||||||
|
|
||||||
|
/* Respect user motion preferences */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html { scroll-behavior: auto; }
|
||||||
|
.animate-drift-slow, .animate-drift-slower { animation: none !important; }
|
||||||
|
.reveal { opacity: 1; transform: none; transition: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance: avoid rendering off-screen content until needed */
|
||||||
|
.content-auto {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 1px 1000px; /* reserve space to prevent big jumps */
|
||||||
|
}
|
||||||
18
frontend/nordic-privacy-ai/app/layout.tsx
Normal file
18
frontend/nordic-privacy-ai/app/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import './globals.css';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Nordic Privacy AI',
|
||||||
|
description: 'AI-powered GDPR compliance platform for Nordic ecosystems',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className="h-full">
|
||||||
|
<body className="min-h-full bg-gradient-to-br from-brand-50 via-white to-brand-100 text-slate-900 antialiased">
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
frontend/nordic-privacy-ai/app/page.tsx
Normal file
59
frontend/nordic-privacy-ai/app/page.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { Navbar } from '../components/Navbar';
|
||||||
|
import { FeatureGrid } from '../components/landing/FeatureGrid';
|
||||||
|
import { AgentsOverview } from '../components/landing/AgentsOverview';
|
||||||
|
import { Steps } from '../components/landing/Steps';
|
||||||
|
import { Footer } from '../components/landing/Footer';
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen flex flex-col">
|
||||||
|
<Navbar />
|
||||||
|
<section className="relative pt-20 pb-24 overflow-hidden">
|
||||||
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||||
|
<div className="absolute -top-24 left-1/2 -translate-x-1/2 w-[480px] h-[480px] md:w-[560px] md:h-[560px] rounded-full bg-gradient-to-br from-brand-200/50 to-brand-400/30 blur-xl opacity-60 animate-drift-slow transform-gpu" />
|
||||||
|
<div className="hidden md:block absolute top-10 right-[10%] w-[340px] h-[340px] rounded-full bg-gradient-to-br from-brand-300/40 to-brand-500/25 blur-xl opacity-50 animate-drift-slower transform-gpu" />
|
||||||
|
<div className="hidden md:block absolute -bottom-28 left-[8%] w-[300px] h-[300px] rounded-full bg-gradient-to-br from-brand-100/60 to-brand-300/35 blur-xl opacity-60 animate-drift-slower transform-gpu" style={{ animationDelay: '1.2s' }} />
|
||||||
|
</div>
|
||||||
|
<div className="container-max relative text-center">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
<h1 className="text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tight text-slate-900">
|
||||||
|
Proactive Nordic Data Privacy
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 text-base sm:text-lg text-slate-700">
|
||||||
|
AI agents that discover, classify, and remediate personal data across your ecosystem—built for BankID, MitID, and Suomi.fi contexts.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex flex-wrap items-center justify-center gap-3 sm:gap-4">
|
||||||
|
<Link href="/try" className="inline-flex items-center rounded-md bg-brand-600 px-7 py-3 text-white font-semibold shadow-lg hover:bg-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">
|
||||||
|
Start free scan
|
||||||
|
</Link>
|
||||||
|
<Link href="#features" className="inline-flex items-center rounded-md border border-slate-300 px-7 py-3 text-slate-700 font-medium hover:bg-white/60 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">
|
||||||
|
Explore features
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 grid grid-cols-3 gap-3 sm:gap-6 text-center text-xs sm:text-sm">
|
||||||
|
<div className="flex flex-col"><span className="font-semibold text-slate-900">99%</span><span className="text-slate-600">PII labels coverage*</span></div>
|
||||||
|
<div className="flex flex-col"><span className="font-semibold text-slate-900"><span className="align-middle">⚡</span> Real-time</span><span className="text-slate-600">Monitoring</span></div>
|
||||||
|
<div className="flex flex-col"><span className="font-semibold text-slate-900">EU-first</span><span className="text-slate-600">Reg alignment</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<FeatureGrid />
|
||||||
|
<Steps />
|
||||||
|
<AgentsOverview />
|
||||||
|
<section id="contact" className="container-max py-20 content-auto">
|
||||||
|
<div className="rounded-2xl border border-brand-200/70 bg-white/80 backdrop-blur p-10 text-center shadow-sm">
|
||||||
|
<h2 className="text-2xl font-bold text-slate-900">Ready to pilot?</h2>
|
||||||
|
<p className="mt-4 text-slate-600 max-w-xl mx-auto">We’re onboarding early Nordic partners. Get in touch to shape proactive privacy intelligence.</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Link href="/try" className="inline-flex items-center rounded-md bg-brand-600 px-6 py-3 text-white font-semibold shadow hover:bg-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">
|
||||||
|
Request access
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/nordic-privacy-ai/app/try/page.tsx
Normal file
23
frontend/nordic-privacy-ai/app/try/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Navbar } from "../../components/Navbar";
|
||||||
|
import { Sidebar, TryTab } from "../../components/try/Sidebar";
|
||||||
|
import { CenterPanel } from "../../components/try/CenterPanel";
|
||||||
|
import { ChatbotPanel } from "../../components/try/ChatbotPanel";
|
||||||
|
|
||||||
|
export default function TryPage() {
|
||||||
|
const [tab, setTab] = useState<TryTab>("processing");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen flex flex-col">
|
||||||
|
<Navbar />
|
||||||
|
<div className="flex flex-1 min-h-0">
|
||||||
|
<Sidebar value={tab} onChange={setTab} />
|
||||||
|
<div className="flex-1 min-h-0 flex">
|
||||||
|
<div className="flex-1 min-h-0"><CenterPanel tab={tab} /></div>
|
||||||
|
<div className="w-[360px] hidden xl:block"><ChatbotPanel /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
frontend/nordic-privacy-ai/components/Navbar.tsx
Normal file
63
frontend/nordic-privacy-ai/components/Navbar.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function Navbar() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const onTry = pathname?.startsWith('/try');
|
||||||
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => setScrolled(window.scrollY > 4);
|
||||||
|
onScroll();
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={`w-full sticky top-0 z-50 transition-colors ${scrolled ? 'bg-white/90 border-b border-slate-200/70 shadow-sm' : 'bg-white/70 border-b border-transparent'}`}>
|
||||||
|
<div className="container-max flex items-center justify-between h-16">
|
||||||
|
<Link href="/" className="font-semibold text-brand-700 text-lg tracking-tight">Nordic Privacy AI</Link>
|
||||||
|
{/* Desktop nav */}
|
||||||
|
{onTry ? (
|
||||||
|
<div className="hidden md:flex items-center gap-6 text-sm">
|
||||||
|
<Link href="/" className="hover:text-brand-600 transition-colors">Home</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="hidden md:flex items-center gap-6 text-sm">
|
||||||
|
<Link href="#features" className="hover:text-brand-600 transition-colors">Features</Link>
|
||||||
|
<Link href="#agents" className="hover:text-brand-600 transition-colors">Agents</Link>
|
||||||
|
<Link href="#contact" className="hover:text-brand-600 transition-colors">Contact</Link>
|
||||||
|
<Link href="/try" className="inline-flex items-center rounded-md bg-brand-600 px-4 py-2 text-white font-medium shadow hover:bg-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">Try me</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<button
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
className="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-md border border-slate-300 text-slate-700"
|
||||||
|
onClick={() => setMenuOpen(v => !v)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Menu</span>
|
||||||
|
{menuOpen ? '✕' : '☰'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Mobile menu panel */}
|
||||||
|
<div className={`md:hidden border-t border-slate-200 transition-[max-height,opacity] duration-200 overflow-hidden ${menuOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'}`}>
|
||||||
|
<div className="container-max py-3 flex flex-col gap-3 text-sm">
|
||||||
|
{onTry ? (
|
||||||
|
<Link href="/" onClick={() => setMenuOpen(false)} className="py-2">Home</Link>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<a href="#features" onClick={() => setMenuOpen(false)} className="py-2">Features</a>
|
||||||
|
<a href="#agents" onClick={() => setMenuOpen(false)} className="py-2">Agents</a>
|
||||||
|
<a href="#contact" onClick={() => setMenuOpen(false)} className="py-2">Contact</a>
|
||||||
|
<Link href="/try" onClick={() => setMenuOpen(false)} className="inline-flex items-center justify-center rounded-md bg-brand-600 px-4 py-2 text-white font-medium shadow hover:bg-brand-500">Try me</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
frontend/nordic-privacy-ai/components/common/Reveal.tsx
Normal file
42
frontend/nordic-privacy-ai/components/common/Reveal.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface RevealProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
as?: keyof JSX.IntrinsicElements;
|
||||||
|
className?: string;
|
||||||
|
delayMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Reveal({ children, as = "div", className, delayMs = 0 }: RevealProps) {
|
||||||
|
const Cmp: any = as;
|
||||||
|
const ref = useRef<HTMLElement | null>(null);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current as Element | null;
|
||||||
|
if (!el || typeof IntersectionObserver === "undefined") {
|
||||||
|
setVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const obs = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (delayMs > 0) {
|
||||||
|
const t = setTimeout(() => setVisible(true), delayMs);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
} else {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.12 }
|
||||||
|
);
|
||||||
|
obs.observe(el);
|
||||||
|
return () => obs.disconnect();
|
||||||
|
}, [delayMs]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Cmp ref={ref} className={(visible ? "reveal-visible " : "reveal ") + (className ?? "")}>{children}</Cmp>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Reveal } from "../common/Reveal";
|
||||||
|
|
||||||
|
export function AgentsOverview() {
|
||||||
|
const agents = [
|
||||||
|
{
|
||||||
|
title: "Discovery Agent",
|
||||||
|
desc:
|
||||||
|
"Continuously inventories systems to locate personal and sensitive data across sources, fixing visibility gaps.",
|
||||||
|
emoji: "🛰️",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Cleaner Agent",
|
||||||
|
desc:
|
||||||
|
"Identifies PII and sensitive attributes, classifies content, and prepares data for remediation and audits.",
|
||||||
|
emoji: "🧽",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Remediation Agent",
|
||||||
|
desc:
|
||||||
|
"Suggests anonymization, consent validation, or deletion; generates compliance reports and monitors posture.",
|
||||||
|
emoji: "🛡️",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container-max py-16 content-auto" id="agents">
|
||||||
|
<Reveal className="text-center" as="div">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900">Our agents</h2>
|
||||||
|
<p className="mt-3 text-slate-600 max-w-2xl mx-auto">
|
||||||
|
Modular, AI-driven roles that work together to keep your data compliant.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 sm:gap-6">
|
||||||
|
{agents.map((a, i) => (
|
||||||
|
<Reveal key={a.title} delayMs={i * 110}>
|
||||||
|
<div className="rounded-xl border border-slate-200 bg-white/80 p-6 shadow-sm transition-transform duration-300 hover:-translate-y-0.5">
|
||||||
|
<div className="text-3xl">{a.emoji}</div>
|
||||||
|
<h3 className="mt-3 font-semibold text-slate-900">{a.title}</h3>
|
||||||
|
<p className="mt-2 text-sm text-slate-600">{a.desc}</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Reveal } from "../common/Reveal";
|
||||||
|
|
||||||
|
export function FeatureGrid() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: "Discovery",
|
||||||
|
desc: "Continuously map data across apps, logs, DBs, and cloud storage to restore visibility.",
|
||||||
|
emoji: "🧭",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Classification",
|
||||||
|
desc: "Detect PII and sensitive categories (health, finance) with AI-driven labeling.",
|
||||||
|
emoji: "🔎",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Remediation",
|
||||||
|
desc: "Anonymize, minimize, and automate consent workflows to reduce exposure.",
|
||||||
|
emoji: "🧹",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Monitoring",
|
||||||
|
desc: "Continuous compliance checks with alerts and reports aligned to GDPR.",
|
||||||
|
emoji: "📈",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container-max py-16 content-auto" id="features">
|
||||||
|
<Reveal className="text-center" as="div">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900">Core capabilities</h2>
|
||||||
|
<p className="mt-3 text-slate-600 max-w-2xl mx-auto">
|
||||||
|
Proactive privacy protection tailored for Nordic identity ecosystems and EU data law.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||||
|
{items.map((f, i) => (
|
||||||
|
<Reveal key={f.title} delayMs={i * 90}>
|
||||||
|
<div className="rounded-xl border border-slate-200 bg-white/80 p-6 shadow-sm transition-transform duration-300 hover:-translate-y-0.5">
|
||||||
|
<div className="text-3xl">{f.emoji}</div>
|
||||||
|
<h3 className="mt-3 font-semibold text-slate-900">{f.title}</h3>
|
||||||
|
<p className="mt-2 text-sm text-slate-600">{f.desc}</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
frontend/nordic-privacy-ai/components/landing/Footer.tsx
Normal file
17
frontend/nordic-privacy-ai/components/landing/Footer.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="mt-24 border-t border-slate-200 bg-white/80">
|
||||||
|
<div className="container-max py-10 flex flex-col sm:flex-row items-center justify-between gap-6">
|
||||||
|
<div className="text-sm text-slate-600">© {new Date().getFullYear()} Nordic Privacy AI. Hackathon prototype.</div>
|
||||||
|
<nav className="flex gap-6 text-sm">
|
||||||
|
<Link href="#features" className="hover:text-brand-600">Features</Link>
|
||||||
|
<Link href="#agents" className="hover:text-brand-600">Agents</Link>
|
||||||
|
<Link href="#contact" className="hover:text-brand-600">Contact</Link>
|
||||||
|
<Link href="/try" className="hover:text-brand-600">Try</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
frontend/nordic-privacy-ai/components/landing/Steps.tsx
Normal file
32
frontend/nordic-privacy-ai/components/landing/Steps.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Reveal } from "../common/Reveal";
|
||||||
|
|
||||||
|
export function Steps() {
|
||||||
|
const steps = [
|
||||||
|
{ title: 'Discover', desc: 'Continuously inventory data across apps, logs, DBs, and cloud.' , emoji: '🧭' },
|
||||||
|
{ title: 'Classify', desc: 'Detect PII and sensitive categories with AI labeling.', emoji: '🔎' },
|
||||||
|
{ title: 'Mitigate', desc: 'Anonymize, minimize, and enforce consent governance.', emoji: '🛡️' },
|
||||||
|
{ title: 'Monitor', desc: 'Continuous checks and reports mapped to GDPR.', emoji: '📈' },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<section className="container-max py-16 content-auto">
|
||||||
|
<Reveal className="text-center" as="div">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900">How it works</h2>
|
||||||
|
<p className="mt-3 text-slate-600 max-w-2xl mx-auto">Simple, proactive steps to keep your data compliant.</p>
|
||||||
|
</Reveal>
|
||||||
|
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6">
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<Reveal key={s.title} delayMs={i * 100}>
|
||||||
|
<div className="relative rounded-xl border border-slate-200 bg-white/80 p-6 shadow-sm transition-transform duration-300 hover:-translate-y-0.5">
|
||||||
|
<div className="text-3xl">{s.emoji}</div>
|
||||||
|
<div className="mt-3 flex items-baseline gap-2">
|
||||||
|
<span className="text-xs text-slate-500">Step {i + 1}</span>
|
||||||
|
<h3 className="font-semibold text-slate-900">{s.title}</h3>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-slate-600">{s.desc}</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
frontend/nordic-privacy-ai/components/landing/ValueProps.tsx
Normal file
21
frontend/nordic-privacy-ai/components/landing/ValueProps.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export function ValueProps() {
|
||||||
|
const items = [
|
||||||
|
{ title: 'EU-first compliance', desc: 'Designed around GDPR principles: lawfulness, purpose limitation, minimization, and accountability.', emoji: '🇪🇺' },
|
||||||
|
{ title: 'Nordic identity ready', desc: 'Built with BankID, MitID, and Suomi.fi contexts in mind for seamless identity-aware workflows.', emoji: '🧩' },
|
||||||
|
{ title: 'Continuous by default', desc: 'Move from manual audits to ongoing monitoring with alerts and clear reports.', emoji: '⏱️' },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<section className="container-max py-16">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900 text-center">Why it fits the Nordics</h2>
|
||||||
|
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{items.map((v) => (
|
||||||
|
<div key={v.title} className="rounded-xl border border-slate-200 bg-white/70 backdrop-blur p-6 shadow-sm">
|
||||||
|
<div className="text-3xl">{v.emoji}</div>
|
||||||
|
<h3 className="mt-3 font-semibold text-slate-900">{v.title}</h3>
|
||||||
|
<p className="mt-2 text-sm text-slate-600">{v.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
238
frontend/nordic-privacy-ai/components/try/CenterPanel.tsx
Normal file
238
frontend/nordic-privacy-ai/components/try/CenterPanel.tsx
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
"use client";
|
||||||
|
import { TryTab } from "./Sidebar";
|
||||||
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
|
import { saveLatestUpload, getLatestUpload, deleteLatestUpload } from "../../lib/idb";
|
||||||
|
|
||||||
|
interface CenterPanelProps {
|
||||||
|
tab: TryTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadedFileMeta {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
contentPreview: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CenterPanel({ tab }: CenterPanelProps) {
|
||||||
|
const [fileMeta, setFileMeta] = useState<UploadedFileMeta | null>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [progress, setProgress] = useState<number>(0);
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [loadedFromCache, setLoadedFromCache] = useState(false);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setFileMeta(null);
|
||||||
|
setProgress(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processFile = useCallback((f: File) => {
|
||||||
|
if (!f) return;
|
||||||
|
setProgress(0);
|
||||||
|
if (f.size > 1024 * 1024) {
|
||||||
|
setFileMeta({
|
||||||
|
name: f.name,
|
||||||
|
size: f.size,
|
||||||
|
type: f.type,
|
||||||
|
contentPreview: "File too large for preview (limit 1MB).",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onprogress = (evt) => {
|
||||||
|
if (evt.lengthComputable) {
|
||||||
|
const pct = Math.min(100, Math.round((evt.loaded / evt.total) * 100));
|
||||||
|
setProgress(pct);
|
||||||
|
} else {
|
||||||
|
setProgress((p) => (p < 90 ? p + 5 : p));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onload = async () => {
|
||||||
|
try {
|
||||||
|
const buf = reader.result as ArrayBuffer;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const text = decoder.decode(buf);
|
||||||
|
const metaObj: UploadedFileMeta = {
|
||||||
|
name: f.name,
|
||||||
|
size: f.size,
|
||||||
|
type: f.type || "unknown",
|
||||||
|
contentPreview: text.slice(0, 4000),
|
||||||
|
};
|
||||||
|
setFileMeta(metaObj);
|
||||||
|
// Save file blob and meta to browser cache (IndexedDB)
|
||||||
|
try {
|
||||||
|
await saveLatestUpload(f, metaObj);
|
||||||
|
} catch {}
|
||||||
|
setProgress(100);
|
||||||
|
} catch (e) {
|
||||||
|
const metaObj: UploadedFileMeta = {
|
||||||
|
name: f.name,
|
||||||
|
size: f.size,
|
||||||
|
type: f.type || "unknown",
|
||||||
|
contentPreview: "Unable to decode preview.",
|
||||||
|
};
|
||||||
|
setFileMeta(metaObj);
|
||||||
|
try {
|
||||||
|
await saveLatestUpload(f, metaObj);
|
||||||
|
} catch {}
|
||||||
|
setProgress(100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
setProgress(0);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(f);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const f = e.target.files?.[0];
|
||||||
|
processFile(f as File);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
const onDragLeave = () => setIsDragging(false);
|
||||||
|
const onDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
const f = e.dataTransfer.files?.[0];
|
||||||
|
processFile(f as File);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load last cached upload on mount (processing tab only)
|
||||||
|
useEffect(() => {
|
||||||
|
let ignore = false;
|
||||||
|
if (tab !== "processing") return;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { meta } = await getLatestUpload();
|
||||||
|
if (!ignore && meta) {
|
||||||
|
setFileMeta(meta as UploadedFileMeta);
|
||||||
|
setLoadedFromCache(true);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
ignore = true;
|
||||||
|
};
|
||||||
|
}, [tab]);
|
||||||
|
|
||||||
|
function renderTabContent() {
|
||||||
|
switch (tab) {
|
||||||
|
case "processing":
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Upload & Process Data</h2>
|
||||||
|
<p className="text-sm text-slate-600">Upload a CSV / JSON / text file. We will later parse, detect PII, and queue analyses.</p>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
onDrop={onDrop}
|
||||||
|
className={
|
||||||
|
"rounded-lg border-2 border-dashed p-6 text-center transition-colors " +
|
||||||
|
(isDragging ? "border-brand-600 bg-brand-50" : "border-slate-300 hover:border-brand-300")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p className="text-sm text-slate-600">Drag & drop a CSV / JSON / TXT here, or click to browse.</p>
|
||||||
|
<div className="mt-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
className="inline-flex items-center rounded-md bg-brand-600 px-4 py-2 text-white text-sm font-medium shadow hover:bg-brand-500"
|
||||||
|
>
|
||||||
|
Choose file
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".csv,.json,.txt"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="hidden"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
{progress > 0 && (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="h-2 w-full rounded-full bg-slate-200 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-2 bg-brand-600 transition-all"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-slate-500">Processing {progress}%</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{fileMeta && (
|
||||||
|
<div className="rounded-md border border-slate-200 p-4 bg-white shadow-sm">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="text-sm font-medium">{fileMeta.name}</div>
|
||||||
|
<div className="text-xs text-slate-500">{Math.round(fileMeta.size / 1024)} KB</div>
|
||||||
|
</div>
|
||||||
|
{loadedFromCache && (
|
||||||
|
<div className="mb-2 text-[11px] text-brand-700">Loaded from browser cache</div>
|
||||||
|
)}
|
||||||
|
<div className="mb-3 text-xs text-slate-500">{fileMeta.type || "Unknown type"}</div>
|
||||||
|
<pre className="max-h-64 overflow-auto text-xs bg-slate-50 p-3 rounded-md whitespace-pre-wrap leading-relaxed">
|
||||||
|
{fileMeta.contentPreview || "(no preview)"}
|
||||||
|
</pre>
|
||||||
|
<div className="mt-3 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
reset();
|
||||||
|
try { await deleteLatestUpload(); } catch {}
|
||||||
|
setLoadedFromCache(false);
|
||||||
|
}}
|
||||||
|
className="text-xs rounded-md border px-3 py-1.5 hover:bg-slate-50"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "bias-analysis":
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Bias Analysis (Placeholder)</h2>
|
||||||
|
<p className="text-sm text-slate-600">Once processing completes, bias metrics will appear here (distribution, representation, fairness indicators).</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "risk-analysis":
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Risk Analysis (Placeholder)</h2>
|
||||||
|
<p className="text-sm text-slate-600">Potential privacy exposure, sensitive attribute concentration, consent gaps will be displayed.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "bias-risk-mitigation":
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Mitigation Suggestions (Placeholder)</h2>
|
||||||
|
<p className="text-sm text-slate-600">Recommended transformations, anonymization strategies, sampling adjustments, consent workflows.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "results":
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Results Summary (Placeholder)</h2>
|
||||||
|
<p className="text-sm text-slate-600">Aggregated findings and downloadable compliance report will appear here.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-y-auto p-6 bg-white/60">
|
||||||
|
{renderTabContent()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
frontend/nordic-privacy-ai/components/try/ChatbotPanel.tsx
Normal file
42
frontend/nordic-privacy-ai/components/try/ChatbotPanel.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ChatbotPanel() {
|
||||||
|
const [messages] = useState<{ role: "user" | "assistant"; content: string }[]>([
|
||||||
|
{ role: "assistant", content: "Hi! I'll help you interpret compliance results soon." },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full border-l border-slate-200 bg-white/80">
|
||||||
|
<div className="h-14 flex items-center px-4 border-b border-slate-200">
|
||||||
|
<h2 className="font-semibold text-sm text-brand-700">Privacy Copilot</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||||
|
{messages.map((m, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={"rounded-md px-3 py-2 text-sm max-w-[80%] " + (m.role === "assistant" ? "bg-brand-600/10 text-brand-800" : "bg-brand-600 text-white ml-auto")}
|
||||||
|
>
|
||||||
|
{m.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="p-3 border-t border-slate-200">
|
||||||
|
<form className="flex gap-2" onSubmit={e => e.preventDefault()}>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
placeholder="Chat coming soon..."
|
||||||
|
className="flex-1 rounded-md border border-slate-300 bg-slate-50 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400 disabled:opacity-60"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled
|
||||||
|
className="rounded-md bg-brand-600 text-white px-4 py-2 text-sm font-medium disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
frontend/nordic-privacy-ai/components/try/Sidebar.tsx
Normal file
50
frontend/nordic-privacy-ai/components/try/Sidebar.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
export type TryTab =
|
||||||
|
| "processing"
|
||||||
|
| "bias-analysis"
|
||||||
|
| "risk-analysis"
|
||||||
|
| "bias-risk-mitigation"
|
||||||
|
| "results";
|
||||||
|
|
||||||
|
const tabs: { id: TryTab; label: string; description: string }[] = [
|
||||||
|
{ id: "processing", label: "Processing", description: "Upload & parse" },
|
||||||
|
{ id: "bias-analysis", label: "Bias Analysis", description: "Detect patterns" },
|
||||||
|
{ id: "risk-analysis", label: "Risk Analysis", description: "Assess exposure" },
|
||||||
|
{ id: "bias-risk-mitigation", label: "Bias & Risk Mitigation", description: "Recommend actions" },
|
||||||
|
{ id: "results", label: "Results", description: "View summaries" },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
value: TryTab;
|
||||||
|
onChange: (tab: TryTab) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sidebar({ value, onChange }: SidebarProps) {
|
||||||
|
return (
|
||||||
|
<aside className={clsx("flex-none w-64 h-full border-r border-slate-200 bg-white/80 flex flex-col")}>
|
||||||
|
<div className="flex items-center px-3 h-14 border-b border-slate-200">
|
||||||
|
<span className="font-semibold text-sm text-brand-700">Workflow</span>
|
||||||
|
</div>
|
||||||
|
<nav className="flex-1 overflow-y-auto py-2 space-y-1">
|
||||||
|
{tabs.map((t) => {
|
||||||
|
const selected = t.id === value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
onClick={() => onChange(t.id)}
|
||||||
|
className={clsx(
|
||||||
|
"group w-full text-left px-4 py-3 text-sm font-medium flex flex-col gap-0.5 transition-colors",
|
||||||
|
selected ? "bg-brand-600/10 text-brand-800" : "hover:bg-brand-50 text-slate-700"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={clsx("", selected && "font-semibold")}>{t.label}</span>
|
||||||
|
<span className="text-xs text-slate-500 group-hover:text-slate-600 line-clamp-1">{t.description}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
frontend/nordic-privacy-ai/next-env.d.ts
vendored
Normal file
5
frontend/nordic-privacy-ai/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
9
frontend/nordic-privacy-ai/next.config.mjs
Normal file
9
frontend/nordic-privacy-ai/next.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
experimental: {
|
||||||
|
typedRoutes: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
6078
frontend/nordic-privacy-ai/package-lock.json
generated
Normal file
6078
frontend/nordic-privacy-ai/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
frontend/nordic-privacy-ai/package.json
Normal file
28
frontend/nordic-privacy-ai/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "nordic-privacy-ai",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"next": "14.2.5",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "20.11.30",
|
||||||
|
"@types/react": "18.2.21",
|
||||||
|
"@types/react-dom": "18.2.7",
|
||||||
|
"autoprefixer": "10.4.17",
|
||||||
|
"eslint": "8.57.0",
|
||||||
|
"eslint-config-next": "14.2.5",
|
||||||
|
"postcss": "8.4.35",
|
||||||
|
"tailwindcss": "3.4.3",
|
||||||
|
"typescript": "5.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/nordic-privacy-ai/postcss.config.js
Normal file
6
frontend/nordic-privacy-ai/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
29
frontend/nordic-privacy-ai/tailwind.config.ts
Normal file
29
frontend/nordic-privacy-ai/tailwind.config.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
50: '#f0f9ff',
|
||||||
|
100: '#e0f2fe',
|
||||||
|
200: '#bae6fd',
|
||||||
|
300: '#7dd3fc',
|
||||||
|
400: '#38bdf8',
|
||||||
|
500: '#0ea5e9',
|
||||||
|
600: '#0284c7',
|
||||||
|
700: '#0369a1',
|
||||||
|
800: '#075985',
|
||||||
|
900: '#0c4a6e'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
41
frontend/nordic-privacy-ai/tsconfig.json
Normal file
41
frontend/nordic-privacy-ai/tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"es2020"
|
||||||
|
],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user