diff --git a/ScreenShots/iPad Air 13-inch/Chat.png b/ScreenShots/iPad Air 13-inch/Chat.png new file mode 100644 index 0000000..e2c56c8 Binary files /dev/null and b/ScreenShots/iPad Air 13-inch/Chat.png differ diff --git a/ScreenShots/iPad Air 13-inch/Home.png b/ScreenShots/iPad Air 13-inch/Home.png new file mode 100644 index 0000000..e823332 Binary files /dev/null and b/ScreenShots/iPad Air 13-inch/Home.png differ diff --git a/ScreenShots/iPad Air 13-inch/Lobby.png b/ScreenShots/iPad Air 13-inch/Lobby.png new file mode 100644 index 0000000..2f100b4 Binary files /dev/null and b/ScreenShots/iPad Air 13-inch/Lobby.png differ diff --git a/ScreenShots/iPad Air 13-inch/Start.png b/ScreenShots/iPad Air 13-inch/Start.png new file mode 100644 index 0000000..ccab001 Binary files /dev/null and b/ScreenShots/iPad Air 13-inch/Start.png differ diff --git a/ScreenShots/iPad Air 13-inch/Transfers.png b/ScreenShots/iPad Air 13-inch/Transfers.png new file mode 100644 index 0000000..7e2ae5d Binary files /dev/null and b/ScreenShots/iPad Air 13-inch/Transfers.png differ diff --git a/ScreenShots/iPhone 17 Pro Max/Chat.png b/ScreenShots/iPhone 17 Pro Max/Chat.png new file mode 100644 index 0000000..d4fdd85 Binary files /dev/null and b/ScreenShots/iPhone 17 Pro Max/Chat.png differ diff --git a/ScreenShots/iPhone 17 Pro Max/Home.png b/ScreenShots/iPhone 17 Pro Max/Home.png new file mode 100644 index 0000000..c4a0dc8 Binary files /dev/null and b/ScreenShots/iPhone 17 Pro Max/Home.png differ diff --git a/ScreenShots/iPhone 17 Pro Max/Lobby.png b/ScreenShots/iPhone 17 Pro Max/Lobby.png new file mode 100644 index 0000000..d11668f Binary files /dev/null and b/ScreenShots/iPhone 17 Pro Max/Lobby.png differ diff --git a/ScreenShots/iPhone 17 Pro Max/Start.png b/ScreenShots/iPhone 17 Pro Max/Start.png new file mode 100644 index 0000000..cc7536f Binary files /dev/null and b/ScreenShots/iPhone 17 Pro Max/Start.png differ diff --git a/ScreenShots/iPhone 17 Pro Max/Transfers.png b/ScreenShots/iPhone 17 Pro Max/Transfers.png new file mode 100644 index 0000000..a63bd59 Binary files /dev/null and b/ScreenShots/iPhone 17 Pro Max/Transfers.png differ diff --git a/bun.lock b/bun.lock index 51dc0ff..aa3c9ab 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,8 @@ "": { "name": "créditmabligopapp", "dependencies": { + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", "qrcode": "^1.5.3", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -26,6 +28,10 @@ "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -42,6 +48,8 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], diff --git a/front/privacy.css b/front/privacy.css new file mode 100644 index 0000000..e0debfa --- /dev/null +++ b/front/privacy.css @@ -0,0 +1,250 @@ +@import url("https://fonts.googleapis.com/css2?family=Fraunces:wght@400;600;700&family=IBM+Plex+Sans:wght@300;400;600&display=swap"); + +:root { + color-scheme: light; + --ink: #1d1b16; + --ink-soft: #5b574e; + --ink-faint: rgba(29, 27, 22, 0.55); + --paper: #f7f1e6; + --paper-strong: #fffaf0; + --paper-alt: #efe5d4; + --accent: #0f766e; + --accent-dark: #0f4c43; + --accent-soft: rgba(15, 118, 110, 0.12); + --border: rgba(29, 27, 22, 0.12); + --shadow: rgba(20, 18, 14, 0.12); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + color: var(--ink); + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; + background: + radial-gradient(circle at top right, rgba(15, 118, 110, 0.12), transparent 45%), + radial-gradient(circle at 20% 20%, rgba(15, 118, 110, 0.08), transparent 40%), + linear-gradient(180deg, var(--paper) 0%, var(--paper-alt) 60%, var(--paper) 100%); + position: relative; + overflow-x: hidden; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(29, 27, 22, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(29, 27, 22, 0.04) 1px, transparent 1px); + background-size: 36px 36px; + mix-blend-mode: multiply; + opacity: 0.45; + pointer-events: none; + z-index: 0; +} + +body::after { + content: ""; + position: fixed; + inset: -20% 0 0 0; + background: radial-gradient(circle at 50% 0%, rgba(0, 0, 0, 0.12), transparent 60%); + opacity: 0.18; + pointer-events: none; + z-index: 0; +} + +.page-privacy #root { + position: relative; + z-index: 1; +} + +.privacy { + max-width: 980px; + margin: 0 auto; + padding: 3.5rem 1.5rem 4rem; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.hero { + background: var(--paper-strong); + border: 1px solid var(--border); + border-radius: 28px; + padding: 2.5rem 2.2rem; + box-shadow: 0 25px 60px var(--shadow); + display: grid; + gap: 1rem; + position: relative; +} + +.hero::after { + content: ""; + position: absolute; + right: 1.6rem; + top: 1.8rem; + width: 92px; + height: 92px; + border-radius: 50%; + border: 2px dashed var(--accent-dark); + opacity: 0.35; + pointer-events: none; +} + +.hero__top { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.back-link { + text-decoration: none; + font-size: 0.95rem; + color: var(--accent-dark); + font-weight: 600; + padding-bottom: 2px; + border-bottom: 1px solid rgba(15, 118, 110, 0.3); +} + +.hero h1 { + font-family: "Fraunces", "Times New Roman", serif; + font-size: clamp(2.4rem, 3.2vw + 1rem, 3.6rem); + margin: 0; + letter-spacing: -0.02em; +} + +.hero p { + margin: 0; + font-size: 1.1rem; + color: var(--ink-soft); + max-width: 32rem; +} + +.hero__meta { + text-transform: uppercase; + letter-spacing: 0.2em; + font-size: 0.72rem; + color: var(--ink-faint); + font-weight: 600; +} + +.lang-toggle { + background: var(--paper-alt); + border-radius: 999px; + padding: 0.25rem; + border: 1px solid var(--border); + display: inline-flex; + gap: 0.25rem; +} + +.lang-toggle button { + border: none; + background: transparent; + padding: 0.4rem 0.9rem; + border-radius: 999px; + font-weight: 600; + font-size: 0.85rem; + color: var(--ink-soft); + cursor: pointer; +} + +.lang-toggle button.active { + background: var(--accent); + color: #fff; + box-shadow: 0 10px 24px rgba(15, 118, 110, 0.25); +} + +.summary { + display: grid; + gap: 0.6rem; + background: rgba(255, 255, 255, 0.65); + border-radius: 20px; + border: 1px solid var(--border); + padding: 1.4rem 1.6rem; + box-shadow: 0 18px 45px rgba(20, 18, 14, 0.08); +} + +.summary__label { + text-transform: uppercase; + font-size: 0.72rem; + letter-spacing: 0.2em; + color: var(--ink-faint); + font-weight: 600; +} + +.summary__text { + font-size: 1.05rem; + line-height: 1.6; + color: var(--ink); +} + +.policy { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +} + +.policy__card { + background: var(--paper-strong); + border-radius: 20px; + border: 1px solid var(--border); + padding: 1.5rem; + box-shadow: 0 18px 40px rgba(20, 18, 14, 0.08); + display: grid; + gap: 0.8rem; +} + +.policy__card h2 { + margin: 0; + font-family: "Fraunces", serif; + font-size: 1.25rem; +} + +.policy__card ul { + margin: 0; + padding-left: 1.1rem; + color: var(--ink-soft); + line-height: 1.5; + display: grid; + gap: 0.6rem; +} + +.footer { + font-size: 0.95rem; + color: var(--ink-soft); + border-top: 1px solid var(--border); + padding-top: 1.4rem; +} + +.reveal { + opacity: 0; + transform: translateY(18px); + animation: reveal 0.7s ease forwards; + animation-delay: var(--delay, 0s); +} + +@keyframes reveal { + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 700px) { + .hero { + padding: 2rem 1.6rem; + } + + .hero::after { + display: none; + } + + .summary { + padding: 1.2rem 1.2rem; + } +} diff --git a/front/privacy.html b/front/privacy.html new file mode 100644 index 0000000..4c2b392 --- /dev/null +++ b/front/privacy.html @@ -0,0 +1,16 @@ + + + + + + Negopoly | Privacy Policy + + + +
+ + + diff --git a/front/privacy.tsx b/front/privacy.tsx new file mode 100644 index 0000000..b8d9203 --- /dev/null +++ b/front/privacy.tsx @@ -0,0 +1,169 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { createRoot } from "react-dom/client"; +import "./privacy.css"; + +type Locale = "en" | "fr"; + +type CopyBlock = { + title: string; + tagline: string; + backLabel: string; + effective: string; + summaryTitle: string; + summaryText: string; + sections: { title: string; items: string[] }[]; + footer: string; +}; + +const copy: Record = { + en: { + title: "Privacy Policy", + tagline: "A tiny policy for a tabletop bank.", + backLabel: "Back to NegoCity", + effective: "Effective February 3, 2026", + summaryTitle: "Quick summary", + summaryText: + "We do not sell personal data. We do not run ads or analytics. We only use the game info needed to run a session.", + sections: [ + { + title: "What we do not do", + items: [ + "We do not sell, trade, or rent personal data.", + "We do not sell your soul (or anyone else's).", + "We do not run ads or third-party analytics.", + "We do not create user accounts.", + ], + }, + { + title: "What we do use to run the game", + items: [ + "Session code, player names, balances, transactions, and chat messages you enter.", + "Optional device notification token, only if you enable push notifications.", + "Electricity.", + ], + }, + { + title: "Where data lives", + items: [ + "Game data lives in memory while the session is active and disappears when it ends.", + "Some data can be stored locally on your device (such as the last session and autosave snapshots).", + "In your head, hopefully.", + ], + }, + ], + footer: "This policy applies to the Negopoly Companion app and companion web experience.", + }, + fr: { + title: "Politique de confidentialite", + tagline: "Une politique simple pour une banque de jeu de societe.", + backLabel: "Retour a NegoCity", + effective: "En vigueur le 3 fevrier 2026", + summaryTitle: "Resume rapide", + summaryText: + "Nous ne vendons pas de donnees personnelles. Nous n'utilisons ni publicites ni analytics. Nous utilisons uniquement les infos de jeu necessaires pour la session.", + sections: [ + { + title: "Ce que nous ne faisons pas", + items: [ + "Nous ne vendons, n'echangeons, ni ne louons vos donnees personnelles.", + "Nous ne vendons pas votre ame (ni celle de qui que ce soit).", + "Nous n'affichons pas de publicites et n'utilisons pas d'analytics tiers.", + "Nous ne creons pas de comptes utilisateurs.", + ], + }, + { + title: "Ce que nous utilisons pour faire tourner la partie", + items: [ + "Code de session, noms des joueurs, soldes, transactions et messages de chat que vous saisissez.", + "Jeton de notification, uniquement si vous activez les notifications push.", + "De l'electricite.", + ], + }, + { + title: "Ou les donnees sont stockees", + items: [ + "Les donnees de jeu restent en memoire pendant la session et disparaissent a la fin.", + "Certaines donnees peuvent etre stockees localement sur votre appareil (par exemple la derniere session et les sauvegardes automatiques).", + "Dans votre tete, esperons-le.", + ], + }, + ], + footer: "Cette politique s'applique a l'application Negopoly Companion et a l'experience web associee.", + }, +}; + +function getDefaultLocale(): Locale { + if (typeof navigator === "undefined") return "en"; + return navigator.language?.toLowerCase().startsWith("fr") ? "fr" : "en"; +} + +function Privacy() { + const [locale, setLocale] = useState(getDefaultLocale()); + const content = useMemo(() => copy[locale], [locale]); + + useEffect(() => { + document.documentElement.lang = locale; + }, [locale]); + + return ( +
+
+
+ + {content.backLabel} + +
+ + +
+
+

{content.title}

+

{content.tagline}

+
{content.effective}
+
+ +
+
{content.summaryTitle}
+
{content.summaryText}
+
+ +
+ {content.sections.map((section, index) => ( +
+

{section.title}

+
    + {section.items.map((item) => ( +
  • {item}
  • + ))} +
+
+ ))} +
+ +
+ {content.footer} +
+
+ ); +} + +const root = createRoot(document.getElementById("root")!); +root.render(); diff --git a/index.ts b/index.ts index 85bcce2..9956713 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ import home from "./front/index.html"; import play from "./front/play.html"; +import privacy from "./front/privacy.html"; import { apiRoutes } from "./server/api"; import { handleSocketMessage, registerSocket, unregisterSocket } from "./server/websocket"; @@ -15,6 +16,7 @@ const server = Bun.serve({ headers: { "Content-Type": "application/json" }, }), "/": home, + "/privacy": privacy, "/play": play, "/play/:sessionId": play, "/play/:sessionId/lobby": play, diff --git a/package.json b/package.json index 7bb5dfe..1326021 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "type": "module", "private": true, "dependencies": { + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", "qrcode": "^1.5.3", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..927c103 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,29 @@ { - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } }