Ajout de la privacy policy (obligatoire pour App Store) et ajout de screenshots pour l'App Store

This commit is contained in:
Feror 2026-02-03 20:53:01 +01:00
parent 41fe80a00e
commit fa1b07e81f
17 changed files with 471 additions and 24 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View file

@ -5,6 +5,8 @@
"": { "": {
"name": "créditmabligopapp", "name": "créditmabligopapp",
"dependencies": { "dependencies": {
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^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/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-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=="], "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=="], "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=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],

250
front/privacy.css Normal file
View file

@ -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;
}
}

16
front/privacy.html Normal file
View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>Negopoly | Privacy Policy</title>
<link rel="stylesheet" href="./privacy.css" />
</head>
<body class="page-privacy">
<div id="root"></div>
<script type="module" src="./privacy.tsx"></script>
</body>
</html>

169
front/privacy.tsx Normal file
View file

@ -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<Locale, CopyBlock> = {
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<Locale>(getDefaultLocale());
const content = useMemo(() => copy[locale], [locale]);
useEffect(() => {
document.documentElement.lang = locale;
}, [locale]);
return (
<div className="privacy">
<header className="hero reveal" style={{ "--delay": "0.05s" } as React.CSSProperties}>
<div className="hero__top">
<a className="back-link" href="/">
{content.backLabel}
</a>
<div className="lang-toggle" role="tablist" aria-label="Language">
<button
type="button"
className={locale === "en" ? "active" : ""}
onClick={() => setLocale("en")}
aria-pressed={locale === "en"}
>
English
</button>
<button
type="button"
className={locale === "fr" ? "active" : ""}
onClick={() => setLocale("fr")}
aria-pressed={locale === "fr"}
>
Francais
</button>
</div>
</div>
<h1>{content.title}</h1>
<p>{content.tagline}</p>
<div className="hero__meta">{content.effective}</div>
</header>
<section className="summary reveal" style={{ "--delay": "0.12s" } as React.CSSProperties}>
<div className="summary__label">{content.summaryTitle}</div>
<div className="summary__text">{content.summaryText}</div>
</section>
<section className="policy">
{content.sections.map((section, index) => (
<article
key={section.title}
className="policy__card reveal"
style={{ "--delay": `${0.18 + index * 0.08}s` } as React.CSSProperties}
>
<h2>{section.title}</h2>
<ul>
{section.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</article>
))}
</section>
<footer className="footer reveal" style={{ "--delay": "0.42s" } as React.CSSProperties}>
{content.footer}
</footer>
</div>
);
}
const root = createRoot(document.getElementById("root")!);
root.render(<Privacy />);

View file

@ -1,5 +1,6 @@
import home from "./front/index.html"; import home from "./front/index.html";
import play from "./front/play.html"; import play from "./front/play.html";
import privacy from "./front/privacy.html";
import { apiRoutes } from "./server/api"; import { apiRoutes } from "./server/api";
import { handleSocketMessage, registerSocket, unregisterSocket } from "./server/websocket"; import { handleSocketMessage, registerSocket, unregisterSocket } from "./server/websocket";
@ -15,6 +16,7 @@ const server = Bun.serve({
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}), }),
"/": home, "/": home,
"/privacy": privacy,
"/play": play, "/play": play,
"/play/:sessionId": play, "/play/:sessionId": play,
"/play/:sessionId/lobby": play, "/play/:sessionId/lobby": play,

View file

@ -4,6 +4,8 @@
"type": "module", "type": "module",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View file

@ -1,29 +1,29 @@
{ {
"compilerOptions": { "compilerOptions": {
// Environment setup & latest features // Environment setup & latest features
"lib": ["ESNext"], "lib": ["ESNext", "DOM"],
"target": "ESNext", "target": "ESNext",
"module": "Preserve", "module": "Preserve",
"moduleDetection": "force", "moduleDetection": "force",
"jsx": "react-jsx", "jsx": "react-jsx",
"allowJs": true, "allowJs": true,
// Bundler mode // Bundler mode
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"noEmit": true, "noEmit": true,
// Best practices // Best practices
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noImplicitOverride": true, "noImplicitOverride": true,
// Some stricter flags (disabled by default) // Some stricter flags (disabled by default)
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false "noPropertyAccessFromIndexSignature": false
} }
} }