testing
This commit is contained in:
28
config/prod/db.php
Normal file
28
config/prod/db.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
// config/db.php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$DB_HOST = 'localhost';
|
||||||
|
$DB_NAME = 'd0455ede';
|
||||||
|
$DB_USER = 'd0455ede';
|
||||||
|
$DB_PASS = 'fF8PhxfCibdLBrSxowIo'; // anpassen
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
|
||||||
|
$DB_USER,
|
||||||
|
$DB_PASS,
|
||||||
|
$options
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// In Produktion Logging, keine Details ausgeben
|
||||||
|
http_response_code(500);
|
||||||
|
echo 'Database connection error.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
28
config/staging/db.php
Normal file
28
config/staging/db.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
// config/db.php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$DB_HOST = 'localhost';
|
||||||
|
$DB_NAME = 'd0455edf';
|
||||||
|
$DB_USER = 'd0455edf';
|
||||||
|
$DB_PASS = 'fF8PhxfCibdLBrSxowIo'; // anpassen
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
|
||||||
|
$DB_USER,
|
||||||
|
$DB_PASS,
|
||||||
|
$options
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// In Produktion Logging, keine Details ausgeben
|
||||||
|
http_response_code(500);
|
||||||
|
echo 'Database connection error.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
91
projektdetails.txt
Normal file
91
projektdetails.txt
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
Projekt Details:
|
||||||
|
Online Angebot einen USB Speicherstick zu überprüfen. Sprich, stimmt die vom Hersteller angegebene Größe mit der tatsächlichen überein.
|
||||||
|
Hierzu soll es zwei Varianten geben:
|
||||||
|
- rein online basiert, die nur Basis checks durchführt
|
||||||
|
- der Nutzer lädt eine Binary herunter, die es der Website ermöglicht eine detailliertere bzw. tiefergehende Prüfung vorzunehmen.
|
||||||
|
|
||||||
|
Ggf. können die Funktionen später erweitert werden.
|
||||||
|
|
||||||
|
Domains:
|
||||||
|
- usbcheck.it
|
||||||
|
- usb-check.it
|
||||||
|
- ismyusbfake.it
|
||||||
|
- ismyusbfake.com
|
||||||
|
- usb-check.com
|
||||||
|
- fakeusbcheck.com
|
||||||
|
|
||||||
|
Domainaufbau:
|
||||||
|
Hauptdomain: usbcheck.it (Staging: staging.usbcheck.it)
|
||||||
|
Domains welche auf diese umgeleitet werden: usb-check.it, usb-check.com
|
||||||
|
|
||||||
|
SocialMedia/LandingPage Domain: ismyusbfake.com (Staging: staging.ismyusbfake.com)
|
||||||
|
Domains welche auf diese umgeleitet werden: ismyusbfake.it, fakeusbcheck.com
|
||||||
|
|
||||||
|
|
||||||
|
Ordnerstruktur usbcheck.it auf Gitlab:
|
||||||
|
config/
|
||||||
|
- prod/ => Nutzung für Configdateien
|
||||||
|
- staging/ => Nutzung für Configdateien staging.
|
||||||
|
public/ => hier liegen die generellen Seiten
|
||||||
|
src/ => Nutzung für generelle Tools (z.B. API)
|
||||||
|
|
||||||
|
Ordnerstruktur usbcheck.it auf Server:
|
||||||
|
web/config/ => Nutzung für Configdateien
|
||||||
|
web/public/ => hier zeigt die usbcheck.it hin
|
||||||
|
web/src/ => Nutzung für generelle Tools (z.B. API)
|
||||||
|
|
||||||
|
Ordnerstruktur staging.usbcheck.it auf Server:
|
||||||
|
staging/config/ => Nutzung für Configdateien
|
||||||
|
staging/public/ => hier zeigt die usbcheck.it hin
|
||||||
|
staging/src/ => Nutzung für generelle Tools (z.B. API)
|
||||||
|
|
||||||
|
|
||||||
|
Die Domain ismyusbfake.com zeigt auf einen SubOrdner in web/public/fakecheck und dient als Landingpage, Layout etc. muss von der Hauptdomain übernommen werden
|
||||||
|
Die Domain staging.ismyusbfake.com zeigt auf einen SubOrdner in staging/public/fakecheck und dient als Landingpage, Layout etc. muss von der Hauptdomain übernommen werden
|
||||||
|
|
||||||
|
|
||||||
|
Farbcodes:
|
||||||
|
{
|
||||||
|
"primary": {
|
||||||
|
"brand_blue": "#0051FF",
|
||||||
|
"deep_gray": "#1A1A1A",
|
||||||
|
"silver": "#C8CBD0"
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"green_check": "#03C160",
|
||||||
|
"error_red": "#E63946",
|
||||||
|
"amber_yellow": "#FFDA3D"
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"light_gray": "#F4F4F4",
|
||||||
|
"very_light_gray": "#FAFAFA",
|
||||||
|
"off_white": "#FFFFFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Typografie:
|
||||||
|
{
|
||||||
|
"typography": {
|
||||||
|
"heading": {
|
||||||
|
"font_family": "Montserrat",
|
||||||
|
"font_weight": 700,
|
||||||
|
"style_name": "Montserrat Bold"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"font_family": "Inter",
|
||||||
|
"font_weight": 400,
|
||||||
|
"style_name": "Inter Regular"
|
||||||
|
},
|
||||||
|
"subheading": {
|
||||||
|
"font_family": "Montserrat",
|
||||||
|
"font_weight": 600,
|
||||||
|
"style_name": "Montserrat SemiBold"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logos:
|
||||||
|
public/img/logo.png => Logo nur mit Domain
|
||||||
|
public/img/logo_slogan.png => Logo mit Domain und Slogan
|
||||||
|
public/img/stick_blank.png => USB Stick ohne Text
|
||||||
177
public/account.php
Normal file
177
public/account.php
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
// public/account.php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/auth.php';
|
||||||
|
|
||||||
|
$lang = auth_get_lang();
|
||||||
|
auth_require_login();
|
||||||
|
|
||||||
|
$user = auth_current_user();
|
||||||
|
$csrfToken = auth_csrf_token();
|
||||||
|
|
||||||
|
$profileErrors = [];
|
||||||
|
$profileSuccess = false;
|
||||||
|
|
||||||
|
// --- Profil-Update (Name + Sprache) ---
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'update_profile') {
|
||||||
|
if (!auth_verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||||
|
$profileErrors['csrf'] = 'Deine Sitzung ist abgelaufen. Bitte Seite neu laden.';
|
||||||
|
} else {
|
||||||
|
$fullName = $_POST['full_name'] ?? '';
|
||||||
|
$preferredLang = $_POST['preferred_lang'] ?? $lang;
|
||||||
|
|
||||||
|
$result = auth_update_profile((int)$user['id'], $fullName, $preferredLang);
|
||||||
|
if ($result['success']) {
|
||||||
|
$profileSuccess = true;
|
||||||
|
$user = auth_current_user(); // neu laden
|
||||||
|
$lang = auth_get_lang(); // kann sich geändert haben
|
||||||
|
} else {
|
||||||
|
$profileErrors = $result['errors'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Avatar-Initialen ---
|
||||||
|
$initials = auth_user_initials($user);
|
||||||
|
$avatarUrl = auth_user_avatar_url($user);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<?php echo htmlspecialchars($lang, ENT_QUOTES); ?>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Mein Konto – USBCheck</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Montserrat:wght@600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Main CSS -->
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css?v=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php
|
||||||
|
$langVar = $lang;
|
||||||
|
include __DIR__ . '/partials/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="page-main">
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="section-title" data-i18n="account_title">Mein Konto</h1>
|
||||||
|
<p class="section-lead" data-i18n="account_intro">
|
||||||
|
Verwalte deine Profildaten und behalte deine USB-Tests im Überblick.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="account-layout">
|
||||||
|
<!-- Sidebar: Avatar + Basisinfos -->
|
||||||
|
<aside class="account-sidebar">
|
||||||
|
<div class="account-avatar">
|
||||||
|
<?php if ($avatarUrl): ?>
|
||||||
|
<img src="<?php echo htmlspecialchars($avatarUrl, ENT_QUOTES); ?>" alt="Avatar">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="avatar-circle">
|
||||||
|
<span><?php echo htmlspecialchars($initials, ENT_QUOTES); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="account-basic-info">
|
||||||
|
<h2><?php echo htmlspecialchars($user['full_name'] ?? $user['username'], ENT_QUOTES); ?></h2>
|
||||||
|
<p class="muted">
|
||||||
|
<?php echo htmlspecialchars($user['email'], ENT_QUOTES); ?>
|
||||||
|
</p>
|
||||||
|
<p class="muted">
|
||||||
|
Username: <?php echo htmlspecialchars($user['username'], ENT_QUOTES); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="account-links">
|
||||||
|
<a class="btn btn-outline" href="/fakecheck/?lang=<?php echo urlencode($lang); ?>" data-i18n="account_start_test">
|
||||||
|
USB-Test starten
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-ghost" href="/logout.php?lang=<?php echo urlencode($lang); ?>" data-i18n="account_logout">
|
||||||
|
Abmelden
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main: Profilformular + Platzhalter für später -->
|
||||||
|
<section class="account-main">
|
||||||
|
<div class="card">
|
||||||
|
<h2 data-i18n="account_profile_heading">Profil</h2>
|
||||||
|
|
||||||
|
<?php if (!empty($profileErrors['csrf'])): ?>
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<?php echo htmlspecialchars($profileErrors['csrf'], ENT_QUOTES); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($profileSuccess): ?>
|
||||||
|
<div class="alert alert-success" data-i18n="account_profile_updated">
|
||||||
|
Profil wurde aktualisiert.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" action="/account.php?lang=<?php echo urlencode($lang); ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken, ENT_QUOTES); ?>">
|
||||||
|
<input type="hidden" name="action" value="update_profile">
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="full_name" data-i18n="account_full_name_label">Vollständiger Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="full_name"
|
||||||
|
name="full_name"
|
||||||
|
required
|
||||||
|
value="<?php echo htmlspecialchars($user['full_name'] ?? '', ENT_QUOTES); ?>"
|
||||||
|
>
|
||||||
|
<?php if (!empty($profileErrors['full_name'])): ?>
|
||||||
|
<p class="form-error"><?php echo htmlspecialchars($profileErrors['full_name'], ENT_QUOTES); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="preferred_lang" data-i18n="account_lang_label">Bevorzugte Sprache</label>
|
||||||
|
<select id="preferred_lang" name="preferred_lang">
|
||||||
|
<?php
|
||||||
|
$pl = $user['preferred_lang'] ?? $lang;
|
||||||
|
?>
|
||||||
|
<option value="de" <?php echo $pl === 'de' ? 'selected' : ''; ?>>Deutsch</option>
|
||||||
|
<option value="en" <?php echo $pl === 'en' ? 'selected' : ''; ?>>English</option>
|
||||||
|
<option value="it" <?php echo $pl === 'it' ? 'selected' : ''; ?>>Italiano</option>
|
||||||
|
<option value="fr" <?php echo $pl === 'fr' ? 'selected' : ''; ?>>Français</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" data-i18n="account_profile_save">
|
||||||
|
Änderungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-muted">
|
||||||
|
<h2 data-i18n="account_usb_heading">Deine USB-Sticks & Testergebnisse</h2>
|
||||||
|
<p class="muted" data-i18n="account_usb_placeholder">
|
||||||
|
Hier wirst du später eine Übersicht deiner registrierten USB-Sticks und Testergebnisse sehen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-muted">
|
||||||
|
<h2 data-i18n="account_avatar_heading">Avatar</h2>
|
||||||
|
<p class="muted" data-i18n="account_avatar_placeholder">
|
||||||
|
Standardmäßig verwenden wir deine Initialen. Später kannst du hier ein eigenes Profilbild hochladen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/assets/js/lang.js?v=1"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
555
public/assets/css/main.css
Normal file
555
public/assets/css/main.css
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
:root {
|
||||||
|
--brand-blue: #0051FF;
|
||||||
|
--deep-gray: #1A1A1A;
|
||||||
|
--silver: #C8CBD0;
|
||||||
|
--green-check: #03C160;
|
||||||
|
--error-red: #E63946;
|
||||||
|
--amber-yellow: #FFDA3D;
|
||||||
|
--light-gray: #F4F4F4;
|
||||||
|
--very-light-gray: #FAFAFA;
|
||||||
|
--off-white: #FFFFFF;
|
||||||
|
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--radius-pill: 999px;
|
||||||
|
--shadow-soft: 0 18px 45px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #222;
|
||||||
|
background: var(--very-light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout helpers */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: rgba(250, 250, 250, 0.92);
|
||||||
|
border-bottom: 1px solid rgba(200, 203, 208, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 36px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav {
|
||||||
|
display: none;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--brand-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Language switcher */
|
||||||
|
|
||||||
|
.lang-switch {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-current {
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
border: 1px solid var(--silver);
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.35rem 0.8rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 120%;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
padding: 0.4rem;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-menu button {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-menu button:hover {
|
||||||
|
background: var(--very-light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
border: none;
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.6rem 1.4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.08s ease, box-shadow 0.08s ease, background 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--brand-blue), #2a73ff);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 81, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 16px 32px rgba(0, 81, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
border: 1px solid var(--silver);
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: var(--very-light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
padding-inline: 1.2rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover {
|
||||||
|
border-color: var(--silver);
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-disabled {
|
||||||
|
background: var(--light-gray);
|
||||||
|
color: #888;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avatar */
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--deep-gray);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-alt {
|
||||||
|
background: var(--very-light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
color: var(--deep-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-lead {
|
||||||
|
max-width: 680px;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #555;
|
||||||
|
margin: 0.25rem 0 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero */
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding-top: 5rem;
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
background: radial-gradient(circle at top left, rgba(0, 81, 255, 0.08), transparent 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
|
||||||
|
gap: 3rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-kicker {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: var(--brand-blue);
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: clamp(2rem, 3vw, 2.6rem);
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
color: var(--deep-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
color: #555;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bullets {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bullets li::before {
|
||||||
|
content: "•";
|
||||||
|
color: var(--green-check);
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-visual {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 1.4rem 1.5rem;
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
border: 1px solid rgba(200, 203, 208, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-logo {
|
||||||
|
width: 180px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-title {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stick img {
|
||||||
|
width: 200px;
|
||||||
|
max-width: 100%;
|
||||||
|
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Steps / Features / Pricing */
|
||||||
|
|
||||||
|
.steps-grid,
|
||||||
|
.features-grid,
|
||||||
|
.pricing-grid,
|
||||||
|
.faq-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-card,
|
||||||
|
.feature-card,
|
||||||
|
.pricing-card,
|
||||||
|
.faq-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 1.3rem 1.4rem;
|
||||||
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 1px solid rgba(230, 230, 230, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-alt .step-card,
|
||||||
|
.section-alt .feature-card,
|
||||||
|
.section-alt .pricing-card,
|
||||||
|
.section-alt .faq-item {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(0, 81, 255, 0.07);
|
||||||
|
color: var(--brand-blue);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-card h3,
|
||||||
|
.feature-card h3,
|
||||||
|
.pricing-card h3 {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-card p,
|
||||||
|
.feature-card p,
|
||||||
|
.pricing-card p,
|
||||||
|
.faq-item p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card ul,
|
||||||
|
.pricing-card ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card li,
|
||||||
|
.pricing-card li {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card li::before,
|
||||||
|
.pricing-card li::before {
|
||||||
|
content: "✔";
|
||||||
|
color: var(--green-check);
|
||||||
|
margin-right: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pro card */
|
||||||
|
|
||||||
|
.feature-card-pro {
|
||||||
|
border-color: rgba(0, 81, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.18rem 0.55rem;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(0, 81, 255, 0.08);
|
||||||
|
color: var(--brand-blue);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pricing */
|
||||||
|
|
||||||
|
.pricing-card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card-muted {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-tag {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin: 0.3rem 0 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FAQ */
|
||||||
|
|
||||||
|
.faq-item summary {
|
||||||
|
font-family: 'Montserrat', system-ui, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item summary::after {
|
||||||
|
content: "+";
|
||||||
|
float: right;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item[open] summary::after {
|
||||||
|
content: "−";
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item p {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
border-top: 1px solid rgba(200, 203, 208, 0.6);
|
||||||
|
background: #fafafa;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.main-nav {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
.hero-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.hero-visual {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
.header-inner {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.steps-grid,
|
||||||
|
.features-grid,
|
||||||
|
.pricing-grid,
|
||||||
|
.faq-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
408
public/assets/js/lang.js
Normal file
408
public/assets/js/lang.js
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
// public/assets/js/lang.js
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
// Meta / Nav
|
||||||
|
meta_title: "USBCheck – Test your USB drives",
|
||||||
|
nav_how_it_works: "How it works",
|
||||||
|
nav_features: "Features",
|
||||||
|
nav_pricing: "Pricing",
|
||||||
|
nav_faq: "FAQ",
|
||||||
|
btn_login: "Login",
|
||||||
|
|
||||||
|
// Hero
|
||||||
|
hero_kicker: "USB drive diagnostics",
|
||||||
|
hero_title: "Check your USB sticks – before you trust them.",
|
||||||
|
hero_subtitle: "USBCheck helps you evaluate capacity, speed and data integrity of your USB drives – with a quick browser-based test and an optional Pro mode for deeper analysis.",
|
||||||
|
cta_quick_test: "Start quick test",
|
||||||
|
cta_how_it_works: "How does it work?",
|
||||||
|
hero_bullet_1: "Quick test directly in your browser – no account required.",
|
||||||
|
hero_bullet_2: "Understand real performance, not just what’s printed on the package.",
|
||||||
|
hero_bullet_3: "Optional advanced tests with a local helper app (coming soon).",
|
||||||
|
hero_card_title: "Example values from a good USB 3.2 drive",
|
||||||
|
metric_speed: "Sequential speed",
|
||||||
|
metric_integrity: "Integrity score",
|
||||||
|
metric_confidence: "Confidence",
|
||||||
|
hero_small_hint: "Values are examples – your real USB drive is measured on your own device.",
|
||||||
|
|
||||||
|
// How it works
|
||||||
|
how_title: "How USBCheck works",
|
||||||
|
how_intro: "USBCheck combines a browser-based quick test with optional advanced tests via a local helper. You decide how deep you want to go – from a simple speed check to extended capacity and integrity tests.",
|
||||||
|
how_step1_title: "1. Choose your drive",
|
||||||
|
how_step1_text: "Start the quick test and select a folder on the USB stick you want to analyse. The browser only sees the folder you explicitly allow.",
|
||||||
|
how_step2_title: "2. Run the quick test",
|
||||||
|
how_step2_text: "USBCheck creates temporary test data, measures write & read speed and checks whether everything can be read back correctly.",
|
||||||
|
how_step3_title: "3. Understand the results",
|
||||||
|
how_step3_text: "You receive a compact report with speed, integrity and hints on how the result compares to typical values for similar drives.",
|
||||||
|
how_step4_title: "4. Go deeper (Pro mode, coming soon)",
|
||||||
|
how_step4_text: "With an optional helper app, advanced tests can check the effective capacity, test larger areas and produce detailed logs – directly controlled from the web interface.",
|
||||||
|
|
||||||
|
// Features
|
||||||
|
features_title: "Designed for transparency and control",
|
||||||
|
features_intro: "Whether you just want a quick check or a detailed analysis – USBCheck gives you control over how your USB drive is tested.",
|
||||||
|
feature_free_title: "Quick test in the browser",
|
||||||
|
feature_free_1: "No installation – everything runs in your browser.",
|
||||||
|
feature_free_2: "Select a folder on the USB drive and run a short write/read test.",
|
||||||
|
feature_free_3: "Get a clear, human-friendly summary of your results.",
|
||||||
|
feature_pro_title: "Pro mode with helper app",
|
||||||
|
feature_pro_1: "Extended tests with larger data volumes (planned).",
|
||||||
|
feature_pro_2: "Optional integration with established tools on your system.",
|
||||||
|
feature_pro_3: "Detailed reports and export options for documentation.",
|
||||||
|
label_coming_soon: "Coming soon",
|
||||||
|
|
||||||
|
// Pricing
|
||||||
|
pricing_title: "Fair model: quick test free, advanced tests optional",
|
||||||
|
pricing_intro: "The quick browser-based check will remain free. For extended tests and power features, we are planning a transparent Pro model.",
|
||||||
|
pricing_free_title: "Quick test",
|
||||||
|
pricing_free_price: "Free",
|
||||||
|
pricing_free_1: "Browser-based speed and integrity test",
|
||||||
|
pricing_free_2: "No registration required",
|
||||||
|
pricing_free_3: "Local test – your data remains on your device",
|
||||||
|
pricing_free_cta: "Start quick test now",
|
||||||
|
pricing_pro_title: "Pro mode",
|
||||||
|
pricing_pro_price: "Planned",
|
||||||
|
pricing_pro_1: "Extended capacity and surface tests",
|
||||||
|
pricing_pro_2: "Detailed logs and export options",
|
||||||
|
pricing_pro_3: "Priority support and more configuration options",
|
||||||
|
pricing_pro_cta: "Pro mode not yet available",
|
||||||
|
|
||||||
|
// FAQ
|
||||||
|
faq_title: "Frequently asked questions",
|
||||||
|
faq_intro: "Here you’ll find answers to common questions about how USBCheck works and how your data is handled.",
|
||||||
|
faq_q1: "Is my data uploaded to usbcheck.it during the test?",
|
||||||
|
faq_a1: "No. The quick test runs in your browser and writes temporary data only to the USB drive you have selected. Test data can be deleted afterwards. Only if you actively choose to share anonymous statistics in the future will any information be sent to the server.",
|
||||||
|
faq_q2: "Can USBCheck detect fake capacity drives?",
|
||||||
|
faq_a2: "The quick test is designed for speed and integrity. For suspicious drives or capacity checks, the planned Pro mode with helper app will offer deeper tests that can uncover inconsistencies between reported and actual capacity.",
|
||||||
|
faq_q3: "Which operating systems are supported?",
|
||||||
|
faq_a3: "The browser-based quick test is available on modern desktop browsers (for example Chromium-based). The Pro mode will support Windows, macOS and Linux via a small helper application.",
|
||||||
|
faq_q4: "Is frequent testing harmful to my USB drive?",
|
||||||
|
faq_a4: "Flash memory has a limited number of write cycles. The quick test uses moderate data volumes. For very intensive or repeated tests with large data sizes, we will explicitly point out the potential wear in the Pro mode.",
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
footer_imprint: "Imprint",
|
||||||
|
footer_privacy: "Privacy"
|
||||||
|
},
|
||||||
|
|
||||||
|
de: {
|
||||||
|
meta_title: "USBCheck – USB-Sticks testen",
|
||||||
|
nav_how_it_works: "Funktionsweise",
|
||||||
|
nav_features: "Funktionen",
|
||||||
|
nav_pricing: "Preise",
|
||||||
|
nav_faq: "FAQ",
|
||||||
|
btn_login: "Login",
|
||||||
|
|
||||||
|
hero_kicker: "USB-Stick-Diagnose",
|
||||||
|
hero_title: "Prüfe deinen USB-Stick – bevor du ihm vertraust.",
|
||||||
|
hero_subtitle: "USBCheck hilft dir, Kapazität, Geschwindigkeit und Datenintegrität deiner USB-Sticks einzuschätzen – mit einem schnellen Browser-Test und einem geplanten Pro-Modus für tiefere Analysen.",
|
||||||
|
cta_quick_test: "Schnelltest starten",
|
||||||
|
cta_how_it_works: "So funktioniert es",
|
||||||
|
hero_bullet_1: "Schneller Test direkt im Browser – ohne Registrierung.",
|
||||||
|
hero_bullet_2: "Verstehe die tatsächliche Leistung, nicht nur die Herstellerangaben.",
|
||||||
|
hero_bullet_3: "Optional erweiterte Tests mit lokaler Helper-App (in Planung).",
|
||||||
|
hero_card_title: "Beispielwerte eines guten USB-3.2-Sticks",
|
||||||
|
metric_speed: "Sequenzielle Geschwindigkeit",
|
||||||
|
metric_integrity: "Integrität",
|
||||||
|
metric_confidence: "Einschätzung",
|
||||||
|
hero_small_hint: "Die gezeigten Werte sind Beispiele – dein eigener USB-Stick wird auf deinem Gerät gemessen.",
|
||||||
|
|
||||||
|
how_title: "So funktioniert USBCheck",
|
||||||
|
how_intro: "USBCheck kombiniert einen schnellen Browser-Test mit optional erweiterten Prüfungen über eine lokale Helper-App. Du bestimmst, wie tief die Analyse gehen soll.",
|
||||||
|
how_step1_title: "1. Stick auswählen",
|
||||||
|
how_step1_text: "Starte den Schnelltest und wähle einen Ordner auf dem USB-Stick, den du prüfen möchtest. Der Browser sieht nur den Ordner, den du freigibst.",
|
||||||
|
how_step2_title: "2. Schnelltest ausführen",
|
||||||
|
how_step2_text: "USBCheck schreibt Testdaten, misst Schreib- und Lesegeschwindigkeit und prüft, ob sich alle Daten fehlerfrei zurücklesen lassen.",
|
||||||
|
how_step3_title: "3. Ergebnisse verstehen",
|
||||||
|
how_step3_text: "Du erhältst einen kompakten Bericht mit Geschwindigkeit, Integrität und Hinweisen, wie dein Stick im Vergleich zu typischen Werten abschneidet.",
|
||||||
|
how_step4_title: "4. Tiefere Tests (Pro-Modus, geplant)",
|
||||||
|
how_step4_text: "Mit einer optionalen Helper-App können größere Bereiche und Kapazität detaillierter geprüft werden – gesteuert über das Web-Interface.",
|
||||||
|
|
||||||
|
features_title: "Transparente Tests für deine USB-Sticks",
|
||||||
|
features_intro: "Ob schneller Check oder detaillierte Analyse – USBCheck gibt dir Kontrolle darüber, wie dein USB-Stick geprüft wird.",
|
||||||
|
feature_free_title: "Schnelltest im Browser",
|
||||||
|
feature_free_1: "Keine Installation – der Test läuft im Browser.",
|
||||||
|
feature_free_2: "Wähle einen Ordner auf dem Stick und starte einen kurzen Schreib-/Lesetest.",
|
||||||
|
feature_free_3: "Erhalte eine verständliche Zusammenfassung deiner Ergebnisse.",
|
||||||
|
feature_pro_title: "Pro-Modus mit Helper-App",
|
||||||
|
feature_pro_1: "Erweiterte Tests mit größeren Datenmengen (geplant).",
|
||||||
|
feature_pro_2: "Optionale Anbindung an vorhandene Tools auf deinem System.",
|
||||||
|
feature_pro_3: "Detaillierte Protokolle und Export-Möglichkeiten.",
|
||||||
|
label_coming_soon: "Bald verfügbar",
|
||||||
|
|
||||||
|
pricing_title: "Faires Modell: Schnelltest kostenlos",
|
||||||
|
pricing_intro: "Der Browser-Schnelltest bleibt kostenlos. Für erweiterte Prüfungen planen wir ein transparentes Pro-Modell.",
|
||||||
|
pricing_free_title: "Schnelltest",
|
||||||
|
pricing_free_price: "Kostenlos",
|
||||||
|
pricing_free_1: "Browserbasierter Geschwindigkeits- und Integritätstest",
|
||||||
|
pricing_free_2: "Keine Registrierung erforderlich",
|
||||||
|
pricing_free_3: "Lokaler Test – deine Daten bleiben auf deinem Gerät",
|
||||||
|
pricing_free_cta: "Jetzt Schnelltest starten",
|
||||||
|
pricing_pro_title: "Pro-Modus",
|
||||||
|
pricing_pro_price: "In Planung",
|
||||||
|
pricing_pro_1: "Erweiterte Kapazitäts- und Oberflächentests",
|
||||||
|
pricing_pro_2: "Detaillierte Logs und Export",
|
||||||
|
pricing_pro_3: "Priorisierter Support und mehr Optionen",
|
||||||
|
pricing_pro_cta: "Pro-Modus noch nicht verfügbar",
|
||||||
|
|
||||||
|
faq_title: "Häufige Fragen",
|
||||||
|
faq_intro: "Hier findest du Antworten auf häufige Fragen zur Funktionsweise von USBCheck und zum Umgang mit deinen Daten.",
|
||||||
|
faq_q1: "Werden meine Daten während des Tests zu usbcheck.it hochgeladen?",
|
||||||
|
faq_a1: "Nein. Der Schnelltest läuft im Browser und schreibt temporäre Testdaten nur auf den von dir ausgewählten USB-Stick. Testdaten können anschließend gelöscht werden. Erst wenn du später ausdrücklich zustimmst, können anonyme Statistiken übertragen werden.",
|
||||||
|
faq_q2: "Kann USBCheck gefälschte Kapazitäten erkennen?",
|
||||||
|
faq_a2: "Der Schnelltest ist auf Geschwindigkeit und Integrität ausgelegt. Für verdächtige Sticks oder Kapazitätsprüfungen ist im Pro-Modus eine tiefere Analyse geplant, die Unstimmigkeiten zwischen angegebener und tatsächlicher Kapazität aufdecken kann.",
|
||||||
|
faq_q3: "Welche Betriebssysteme werden unterstützt?",
|
||||||
|
faq_a3: "Der Browser-Schnelltest funktioniert auf modernen Desktop-Browsern (zum Beispiel Chromium-basiert). Der Pro-Modus wird eine kleine Helper-App für Windows, macOS und Linux nutzen.",
|
||||||
|
faq_q4: "Schadet häufiges Testen meinem USB-Stick?",
|
||||||
|
faq_a4: "Flash-Speicher hat nur eine begrenzte Zahl an Schreibzyklen. Der Schnelltest verwendet moderate Datenmengen. Bei sehr intensiven oder wiederholten Tests mit großen Volumina weisen wir im Pro-Modus explizit auf möglichen Verschleiß hin.",
|
||||||
|
|
||||||
|
footer_imprint: "Impressum",
|
||||||
|
footer_privacy: "Datenschutz"
|
||||||
|
},
|
||||||
|
|
||||||
|
it: {
|
||||||
|
meta_title: "USBCheck – Test dei supporti USB",
|
||||||
|
nav_how_it_works: "Come funziona",
|
||||||
|
nav_features: "Funzionalità",
|
||||||
|
nav_pricing: "Prezzi",
|
||||||
|
nav_faq: "FAQ",
|
||||||
|
btn_login: "Login",
|
||||||
|
|
||||||
|
hero_kicker: "Diagnostica delle unità USB",
|
||||||
|
hero_title: "Controlla le tue chiavette USB prima di fidarti.",
|
||||||
|
hero_subtitle: "USBCheck ti aiuta a valutare capacità, velocità e integrità dei dati delle tue unità USB – con un rapido test nel browser e una modalità Pro pianificata per analisi più approfondite.",
|
||||||
|
cta_quick_test: "Avvia test rapido",
|
||||||
|
cta_how_it_works: "Come funziona",
|
||||||
|
hero_bullet_1: "Test rapido direttamente nel browser – nessuna registrazione.",
|
||||||
|
hero_bullet_2: "Comprendi le prestazioni reali, non solo le promesse sulla confezione.",
|
||||||
|
hero_bullet_3: "Test avanzati con app locale di supporto (in arrivo).",
|
||||||
|
hero_card_title: "Valori di esempio di una buona unità USB 3.2",
|
||||||
|
metric_speed: "Velocità sequenziale",
|
||||||
|
metric_integrity: "Integrità",
|
||||||
|
metric_confidence: "Valutazione",
|
||||||
|
hero_small_hint: "I valori mostrati sono di esempio – la tua unità USB viene misurata direttamente sul tuo dispositivo.",
|
||||||
|
|
||||||
|
how_title: "Come funziona USBCheck",
|
||||||
|
how_intro: "USBCheck combina un test rapido nel browser con test avanzati opzionali tramite un'app locale. Sei tu a decidere quanto approfondita debba essere l'analisi.",
|
||||||
|
how_step1_title: "1. Seleziona l'unità",
|
||||||
|
how_step1_text: "Avvia il test rapido e seleziona una cartella sulla chiavetta USB che desideri analizzare. Il browser vede solo la cartella che autorizzi.",
|
||||||
|
how_step2_title: "2. Esegui il test",
|
||||||
|
how_step2_text: "USBCheck scrive dati di prova, misura la velocità di scrittura e lettura e controlla che tutto possa essere letto correttamente.",
|
||||||
|
how_step3_title: "3. Analizza i risultati",
|
||||||
|
how_step3_text: "Ricevi un report compatto con velocità, integrità e indicazioni su come la tua unità si confronta con valori tipici.",
|
||||||
|
how_step4_title: "4. Analisi più approfondite (modalità Pro)",
|
||||||
|
how_step4_text: "Con un'app di supporto opzionale, test estesi possono verificare capacità effettiva e porzioni più grandi dell'unità – gestiti dall'interfaccia web.",
|
||||||
|
|
||||||
|
features_title: "Trasparenza e controllo sui tuoi supporti USB",
|
||||||
|
features_intro: "Che tu voglia solo un controllo veloce o un’analisi dettagliata, USBCheck ti offre il livello di test adatto alle tue esigenze.",
|
||||||
|
feature_free_title: "Test rapido nel browser",
|
||||||
|
feature_free_1: "Nessuna installazione – il test avviene nel browser.",
|
||||||
|
feature_free_2: "Seleziona una cartella sull’unità USB ed esegui un breve test di scrittura/lettura.",
|
||||||
|
feature_free_3: "Ottieni un riepilogo chiaro e comprensibile dei risultati.",
|
||||||
|
feature_pro_title: "Modalità Pro con app locale",
|
||||||
|
feature_pro_1: "Test estesi con volumi di dati maggiori (in arrivo).",
|
||||||
|
feature_pro_2: "Integrazione opzionale con strumenti disponibili sul sistema.",
|
||||||
|
feature_pro_3: "Report dettagliati ed esportazione dei risultati.",
|
||||||
|
label_coming_soon: "Prossimamente",
|
||||||
|
|
||||||
|
pricing_title: "Modello equo: test rapido gratuito",
|
||||||
|
pricing_intro: "Il test rapido basato su browser rimarrà gratuito. Per test avanzati stiamo pianificando una modalità Pro trasparente.",
|
||||||
|
pricing_free_title: "Test rapido",
|
||||||
|
pricing_free_price: "Gratuito",
|
||||||
|
pricing_free_1: "Test di velocità e integrità nel browser",
|
||||||
|
pricing_free_2: "Senza registrazione",
|
||||||
|
pricing_free_3: "Test locale – i tuoi dati restano sul dispositivo",
|
||||||
|
pricing_free_cta: "Avvia test rapido",
|
||||||
|
pricing_pro_title: "Modalità Pro",
|
||||||
|
pricing_pro_price: "In arrivo",
|
||||||
|
pricing_pro_1: "Test di capacità e superficie più approfonditi",
|
||||||
|
pricing_pro_2: "Log dettagliati ed esportazione",
|
||||||
|
pricing_pro_3: "Supporto prioritario e maggiori opzioni",
|
||||||
|
pricing_pro_cta: "Modalità Pro non ancora disponibile",
|
||||||
|
|
||||||
|
faq_title: "Domande frequenti",
|
||||||
|
faq_intro: "Qui trovi le risposte alle domande più comuni su come funziona USBCheck e su come vengono gestiti i tuoi dati.",
|
||||||
|
faq_q1: "I miei dati vengono caricati su usbcheck.it durante il test?",
|
||||||
|
faq_a1: "No. Il test rapido viene eseguito nel browser e scrive dati di prova solo sull’unità USB selezionata. I dati di test possono essere eliminati dopo. Solo se in futuro sceglierai esplicitamente di condividere statistiche anonime saranno inviati dati al server.",
|
||||||
|
faq_q2: "USBCheck può rilevare unità con capacità falsa?",
|
||||||
|
faq_a2: "Il test rapido è pensato per velocità e integrità. Per unità sospette o controlli di capacità, la modalità Pro offrirà test più approfonditi per individuare discrepanze tra capacità dichiarata e reale.",
|
||||||
|
faq_q3: "Quali sistemi operativi sono supportati?",
|
||||||
|
faq_a3: "Il test rapido funziona sui moderni browser desktop (ad esempio basati su Chromium). La modalità Pro utilizzerà una piccola app locale per Windows, macOS e Linux.",
|
||||||
|
faq_q4: "Test frequenti possono danneggiare la chiavetta USB?",
|
||||||
|
faq_a4: "La memoria flash ha un numero limitato di cicli di scrittura. Il test rapido utilizza volumi di dati moderati. Per test molto intensivi e ripetuti con grandi quantità di dati, nella modalità Pro segnaleremo chiaramente il possibile usura.",
|
||||||
|
|
||||||
|
footer_imprint: "Note legali",
|
||||||
|
footer_privacy: "Privacy"
|
||||||
|
},
|
||||||
|
|
||||||
|
fr: {
|
||||||
|
meta_title: "USBCheck – Tester vos clés USB",
|
||||||
|
nav_how_it_works: "Fonctionnement",
|
||||||
|
nav_features: "Fonctionnalités",
|
||||||
|
nav_pricing: "Tarifs",
|
||||||
|
nav_faq: "FAQ",
|
||||||
|
btn_login: "Connexion",
|
||||||
|
|
||||||
|
hero_kicker: "Diagnostic de clés USB",
|
||||||
|
hero_title: "Testez vos clés USB avant de leur confier vos données.",
|
||||||
|
hero_subtitle: "USBCheck vous aide à évaluer la capacité, la vitesse et l’intégrité des données de vos clés USB – avec un test rapide dans le navigateur et une future version Pro pour des analyses plus poussées.",
|
||||||
|
cta_quick_test: "Lancer le test rapide",
|
||||||
|
cta_how_it_works: "Comment ça marche ?",
|
||||||
|
hero_bullet_1: "Test rapide directement dans le navigateur – sans inscription.",
|
||||||
|
hero_bullet_2: "Comprenez les performances réelles, pas seulement ce qui est écrit sur l’emballage.",
|
||||||
|
hero_bullet_3: "Tests avancés avec une application locale (bientôt disponible).",
|
||||||
|
hero_card_title: "Valeurs d’exemple pour une bonne clé USB 3.2",
|
||||||
|
metric_speed: "Vitesse séquentielle",
|
||||||
|
metric_integrity: "Intégrité",
|
||||||
|
metric_confidence: "Évaluation",
|
||||||
|
hero_small_hint: "Les valeurs affichées sont des exemples – votre propre clé USB est mesurée directement sur votre appareil.",
|
||||||
|
|
||||||
|
how_title: "Comment fonctionne USBCheck",
|
||||||
|
how_intro: "USBCheck combine un test rapide dans le navigateur avec des tests avancés optionnels via une application locale. Vous choisissez le niveau d’analyse souhaité.",
|
||||||
|
how_step1_title: "1. Choisissez la clé",
|
||||||
|
how_step1_text: "Lancez le test rapide et sélectionnez un dossier sur la clé USB à analyser. Le navigateur n’accède qu’au dossier que vous autorisez.",
|
||||||
|
how_step2_title: "2. Exécutez le test",
|
||||||
|
how_step2_text: "USBCheck écrit des données de test, mesure la vitesse d’écriture et de lecture, puis vérifie si tout peut être relu correctement.",
|
||||||
|
how_step3_title: "3. Interprétez les résultats",
|
||||||
|
how_step3_text: "Vous obtenez un rapport synthétique avec vitesse, intégrité et des indications pour situer votre clé par rapport à des valeurs typiques.",
|
||||||
|
how_step4_title: "4. Analyses avancées (mode Pro)",
|
||||||
|
how_step4_text: "Avec une application locale optionnelle, des tests plus approfondis peuvent vérifier la capacité effective et de plus grandes zones – le tout piloté via l’interface web.",
|
||||||
|
|
||||||
|
features_title: "Transparence et contrôle pour vos clés USB",
|
||||||
|
features_intro: "Que vous ayez besoin d’un simple contrôle ou d’une analyse détaillée, USBCheck vous laisse décider du niveau de test.",
|
||||||
|
feature_free_title: "Test rapide dans le navigateur",
|
||||||
|
feature_free_1: "Aucune installation – tout se fait dans le navigateur.",
|
||||||
|
feature_free_2: "Sélectionnez un dossier sur la clé USB et lancez un court test d’écriture/lecture.",
|
||||||
|
feature_free_3: "Recevez un résumé clair et compréhensible des résultats.",
|
||||||
|
feature_pro_title: "Mode Pro avec application locale",
|
||||||
|
feature_pro_1: "Tests étendus avec un volume de données plus important (bientôt).",
|
||||||
|
feature_pro_2: "Intégration optionnelle avec des outils présents sur votre système.",
|
||||||
|
feature_pro_3: "Rapports détaillés et options d’export.",
|
||||||
|
label_coming_soon: "Bientôt disponible",
|
||||||
|
|
||||||
|
pricing_title: "Modèle équitable : test rapide gratuit",
|
||||||
|
pricing_intro: "Le test rapide dans le navigateur restera gratuit. Pour les tests avancés, un mode Pro transparent est prévu.",
|
||||||
|
pricing_free_title: "Test rapide",
|
||||||
|
pricing_free_price: "Gratuit",
|
||||||
|
pricing_free_1: "Test de vitesse et d’intégrité dans le navigateur",
|
||||||
|
pricing_free_2: "Aucune inscription requise",
|
||||||
|
pricing_free_3: "Test local – vos données restent sur votre appareil",
|
||||||
|
pricing_free_cta: "Lancer le test rapide",
|
||||||
|
pricing_pro_title: "Mode Pro",
|
||||||
|
pricing_pro_price: "À venir",
|
||||||
|
pricing_pro_1: "Tests de capacité et de surface plus approfondis",
|
||||||
|
pricing_pro_2: "Journalisation détaillée et export",
|
||||||
|
pricing_pro_3: "Support prioritaire et réglages avancés",
|
||||||
|
pricing_pro_cta: "Mode Pro pas encore disponible",
|
||||||
|
|
||||||
|
faq_title: "Questions fréquentes",
|
||||||
|
faq_intro: "Voici des réponses aux questions les plus fréquentes sur le fonctionnement d’USBCheck et la gestion de vos données.",
|
||||||
|
faq_q1: "Mes données sont-elles envoyées à usbcheck.it pendant le test ?",
|
||||||
|
faq_a1: "Non. Le test rapide s’exécute dans le navigateur et écrit des données de test uniquement sur la clé USB que vous avez choisie. Ces données peuvent être supprimées ensuite. Seules des statistiques anonymes pourront être envoyées si vous l’acceptez explicitement à l’avenir.",
|
||||||
|
faq_q2: "USBCheck peut-il détecter une capacité falsifiée ?",
|
||||||
|
faq_a2: "Le test rapide est axé sur la vitesse et l’intégrité. Pour les clés suspectes ou les vérifications de capacité, le mode Pro offrira des tests plus approfondis afin de détecter des incohérences entre capacité annoncée et réelle.",
|
||||||
|
faq_q3: "Quels systèmes d’exploitation sont supportés ?",
|
||||||
|
faq_a3: "Le test rapide fonctionne sur les navigateurs modernes de bureau (par exemple basés sur Chromium). Le mode Pro utilisera une petite application locale pour Windows, macOS et Linux.",
|
||||||
|
faq_q4: "Des tests fréquents peuvent-ils endommager ma clé USB ?",
|
||||||
|
faq_a4: "La mémoire flash dispose d’un nombre limité de cycles d’écriture. Le test rapide utilise un volume de données modéré. Pour des tests très intensifs et répétés avec beaucoup de données, nous signalerons explicitement ce risque dans le mode Pro.",
|
||||||
|
|
||||||
|
footer_imprint: "Mentions légales",
|
||||||
|
footer_privacy: "Confidentialité"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to get lang from URL
|
||||||
|
function getLangFromUrl() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const l = (params.get('lang') || "").toLowerCase();
|
||||||
|
if (["de", "en", "it", "fr"].includes(l)) return l;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to detect from browser
|
||||||
|
function detectBrowserLang() {
|
||||||
|
const navLang = (navigator.language || navigator.userLanguage || "en").toLowerCase();
|
||||||
|
if (navLang.startsWith("de")) return "de";
|
||||||
|
if (navLang.startsWith("it")) return "it";
|
||||||
|
if (navLang.startsWith("fr")) return "fr";
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLang(lang) {
|
||||||
|
const dict = translations[lang] || translations.en;
|
||||||
|
|
||||||
|
// Update <html lang> and <title>
|
||||||
|
document.documentElement.lang = lang;
|
||||||
|
if (dict.meta_title) {
|
||||||
|
document.title = dict.meta_title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update text nodes
|
||||||
|
document.querySelectorAll("[data-i18n]").forEach(el => {
|
||||||
|
const key = el.getAttribute("data-i18n");
|
||||||
|
const text = dict[key];
|
||||||
|
if (typeof text === "string") {
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Language switcher label
|
||||||
|
const currentBtn = document.getElementById("lang-current");
|
||||||
|
if (currentBtn) {
|
||||||
|
currentBtn.textContent = lang.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update quick-test link to keep lang parameter if present
|
||||||
|
const quickBtn = document.getElementById("quick-test-btn");
|
||||||
|
if (quickBtn) {
|
||||||
|
const base = "/fakecheck/";
|
||||||
|
// optional: append ?lang=xx for deep links
|
||||||
|
quickBtn.href = base + "?lang=" + lang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLangSwitcher(currentLang) {
|
||||||
|
const trigger = document.getElementById("lang-current");
|
||||||
|
const menu = document.getElementById("lang-menu");
|
||||||
|
if (!trigger || !menu) return;
|
||||||
|
|
||||||
|
trigger.addEventListener("click", () => {
|
||||||
|
menu.classList.toggle("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.querySelectorAll("button[data-lang]").forEach(btn => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
const newLang = btn.getAttribute("data-lang");
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("lang", newLang);
|
||||||
|
window.location.href = url.toString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (!menu.contains(e.target) && e.target !== trigger) {
|
||||||
|
menu.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLoginAvatar() {
|
||||||
|
const loginBtn = document.getElementById("login-button");
|
||||||
|
const avatar = document.getElementById("user-avatar");
|
||||||
|
if (!loginBtn || !avatar) return;
|
||||||
|
|
||||||
|
// Demo: toggle avatar on click; später durch echte Session-Logik ersetzen
|
||||||
|
loginBtn.addEventListener("click", () => {
|
||||||
|
// Hier würdest du redirect zum echten Login einbauen.
|
||||||
|
// Für das Layout-Demo simulieren wir einfach "eingeloggt".
|
||||||
|
loginBtn.classList.add("hidden");
|
||||||
|
avatar.classList.remove("hidden");
|
||||||
|
// Avatar-Initialen könnten später vom Usernamen kommen.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const urlLang = getLangFromUrl();
|
||||||
|
const lang = urlLang || detectBrowserLang();
|
||||||
|
applyLang(lang);
|
||||||
|
initLangSwitcher(lang);
|
||||||
|
initLoginAvatar();
|
||||||
|
});
|
||||||
785
public/index.php
785
public/index.php
@@ -1,601 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
// public/index.php
|
||||||
|
|
||||||
|
// Einfache Sprach-Erkennung
|
||||||
|
$supportedLangs = ['de', 'en', 'it', 'fr'];
|
||||||
|
$defaultLang = 'en';
|
||||||
|
|
||||||
|
// 1) Direkt per ?lang=de
|
||||||
|
if (isset($_GET['lang']) && in_array($_GET['lang'], $supportedLangs, true)) {
|
||||||
|
$currentLang = $_GET['lang'];
|
||||||
|
} else {
|
||||||
|
// 2) Grobe Erkennung aus Accept-Language
|
||||||
|
$currentLang = $defaultLang;
|
||||||
|
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||||
|
$accepted = strtolower(substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2));
|
||||||
|
if (in_array($accepted, $supportedLangs, true)) {
|
||||||
|
$currentLang = $accepted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="<?php echo htmlspecialchars($currentLang); ?>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8">
|
||||||
<title>FakeUSBCheck – Test USB drives for fakes and hidden problems</title>
|
<title>USBCheck – Test your USB drives</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="description"
|
|
||||||
content="FakeUSBCheck helps you test USB flash drives for fake capacity, slow performance and data integrity issues. Run a quick browser-based check or unlock the Pro mode for deep tests." />
|
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
<!-- Fonts: Montserrat + Inter -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Montserrat:wght@600;700&display=swap" rel="stylesheet">
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Montserrat:wght@600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Tailwind via CDN -->
|
<!-- Main stylesheet -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<link rel="stylesheet" href="/assets/css/main.css?v=1">
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
brand_blue: '#0051FF',
|
|
||||||
deep_gray: '#1A1A1A',
|
|
||||||
silver: '#C8CBD0',
|
|
||||||
green_check: '#03C160',
|
|
||||||
error_red: '#E63946',
|
|
||||||
amber_yellow: '#FFDA3D',
|
|
||||||
light_gray: '#F4F4F4',
|
|
||||||
very_light_gray: '#FAFAFA',
|
|
||||||
off_white: '#FFFFFF'
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
heading: ['Montserrat', 'system-ui', 'sans-serif'],
|
|
||||||
body: ['Inter', 'system-ui', 'sans-serif']
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
soft: '0 20px 40px rgba(0,0,0,0.08)'
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
'2xl': '1rem',
|
|
||||||
'3xl': '1.5rem'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
||||||
}
|
|
||||||
.heading-font {
|
|
||||||
font-family: 'Montserrat', system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
/* Smooth anchor scroll */
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-very_light_gray text-deep_gray">
|
<body data-current-lang="<?php echo htmlspecialchars($currentLang); ?>">
|
||||||
<!-- Page wrapper -->
|
|
||||||
<div class="min-h-screen flex flex-col">
|
|
||||||
|
|
||||||
<!-- Header -->
|
<?php
|
||||||
<header class="sticky top-0 z-40 bg-off_white/90 backdrop-blur border-b border-silver/30">
|
// Header-Partial einbinden (benutzt $currentLang)
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
include __DIR__ . '/partials/header.php';
|
||||||
<div class="flex items-center justify-between h-16">
|
?>
|
||||||
<!-- Logo -->
|
|
||||||
<a href="#" class="flex items-center gap-3">
|
|
||||||
<img src="/img/logo_slogan.png"
|
|
||||||
alt="FakeUSBCheck logo"
|
|
||||||
class="h-9 w-auto" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
<main>
|
||||||
<nav class="hidden md:flex items-center gap-6 text-sm font-medium">
|
<!-- HERO -->
|
||||||
<a href="#how-it-works" class="text-deep_gray/80 hover:text-deep_gray">How it works</a>
|
<section class="section hero" id="top">
|
||||||
<a href="#features" class="text-deep_gray/80 hover:text-deep_gray">Features</a>
|
<div class="container hero-grid">
|
||||||
<a href="#pricing" class="text-deep_gray/80 hover:text-deep_gray">Free vs Pro</a>
|
<div class="hero-text">
|
||||||
<a href="#security" class="text-deep_gray/80 hover:text-deep_gray">Security</a>
|
<p class="hero-kicker" data-i18n="hero_kicker"></p>
|
||||||
<a href="#faq" class="text-deep_gray/80 hover:text-deep_gray">FAQ</a>
|
<h1 class="hero-title" data-i18n="hero_title"></h1>
|
||||||
</nav>
|
<p class="hero-subtitle" data-i18n="hero_subtitle"></p>
|
||||||
|
|
||||||
<!-- Login / Avatar -->
|
<div class="hero-actions">
|
||||||
<div class="flex items-center gap-3">
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/fakecheck/', $currentLang)); ?>"
|
||||||
<!-- This button is shown while logged OUT -->
|
class="btn btn-primary"
|
||||||
<button id="loginButton"
|
id="quick-test-btn"
|
||||||
class="hidden sm:inline-flex items-center gap-2 px-4 py-2 rounded-full border border-brand_blue text-brand_blue text-sm font-medium hover:bg-brand_blue/5 transition">
|
data-i18n="cta_quick_test"></a>
|
||||||
<span>Log in</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Avatar shown while logged IN (initials or icon) -->
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/', $currentLang)); ?>#how-it-works"
|
||||||
<button id="userAvatar"
|
class="btn btn-ghost"
|
||||||
class="hidden sm:inline-flex items-center justify-center w-9 h-9 rounded-full bg-brand_blue text-off_white text-sm font-semibold shadow-soft">
|
data-i18n="cta_how_it_works"></a>
|
||||||
<span id="userAvatarInitials">LG</span>
|
</div>
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Mobile login icon -->
|
<ul class="hero-bullets">
|
||||||
<button id="loginIconMobile"
|
<li data-i18n="hero_bullet_1"></li>
|
||||||
class="inline-flex sm:hidden items-center justify-center w-9 h-9 rounded-full border border-brand_blue text-brand_blue hover:bg-brand_blue/5 transition"
|
<li data-i18n="hero_bullet_2"></li>
|
||||||
aria-label="Log in">
|
<li data-i18n="hero_bullet_3"></li>
|
||||||
<!-- Simple icon -->
|
</ul>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
</div>
|
||||||
stroke="currentColor" stroke-width="1.8">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<div class="hero-visual">
|
||||||
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-9A2.25 2.25 0 002.25 5.25v13.5A2.25 2.25 0 004.5 21h9a2.25 2.25 0 002.25-2.25V15M18 9l3 3m0 0l-3 3m3-3H9"/>
|
<div class="hero-card">
|
||||||
</svg>
|
<img src="/img/logo_slogan.png" alt="usbcheck.it" class="hero-logo">
|
||||||
</button>
|
<p class="hero-card-title" data-i18n="hero_card_title"></p>
|
||||||
|
<div class="hero-metrics">
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label" data-i18n="metric_speed"></span>
|
||||||
|
<span class="metric-value">125 MB/s</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label" data-i18n="metric_integrity"></span>
|
||||||
|
<span class="metric-value">99.98%</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label" data-i18n="metric_confidence"></span>
|
||||||
|
<span class="metric-value">✔</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="hero-small" data-i18n="hero_small_hint"></p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-stick">
|
||||||
|
<img src="/img/stick_blank.png" alt="USB Stick Illustration">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</section>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- HOW IT WORKS -->
|
||||||
<main class="flex-1">
|
<section class="section" id="how-it-works">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title" data-i18n="how_title"></h2>
|
||||||
|
<p class="section-lead" data-i18n="how_intro"></p>
|
||||||
|
|
||||||
<!-- HERO -->
|
<div class="steps-grid">
|
||||||
<section class="bg-gradient-to-b from-off_white to-very_light_gray pt-12 pb-20">
|
<article class="step-card">
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col lg:flex-row items-center gap-10">
|
<div class="step-icon">1</div>
|
||||||
<!-- Hero text -->
|
<h3 data-i18n="how_step1_title"></h3>
|
||||||
<div class="flex-1">
|
<p data-i18n="how_step1_text"></p>
|
||||||
<span class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-light_gray text-xs font-medium text-deep_gray/70 mb-4">
|
</article>
|
||||||
<span class="w-2 h-2 rounded-full bg-green_check"></span>
|
<article class="step-card">
|
||||||
Detect fake USB drives before they destroy your data
|
<div class="step-icon">2</div>
|
||||||
</span>
|
<h3 data-i18n="how_step2_title"></h3>
|
||||||
|
<p data-i18n="how_step2_text"></p>
|
||||||
<h1 class="heading-font text-3xl sm:text-4xl lg:text-5xl font-bold text-deep_gray mb-4">
|
</article>
|
||||||
Test your USB sticks for <span class="text-brand_blue">fakes, slow speed</span> and
|
<article class="step-card">
|
||||||
<span class="text-brand_blue">hidden errors</span>.
|
<div class="step-icon">3</div>
|
||||||
</h1>
|
<h3 data-i18n="how_step3_title"></h3>
|
||||||
|
<p data-i18n="how_step3_text"></p>
|
||||||
<p class="text-base sm:text-lg text-deep_gray/80 max-w-xl mb-6">
|
</article>
|
||||||
FakeUSBCheck helps you verify the real capacity and stability of USB flash drives.
|
<article class="step-card">
|
||||||
Run a fast browser-based check in minutes or unlock the Pro mode for deep surface tests,
|
<div class="step-icon">4</div>
|
||||||
inspired by tools like <strong>f3</strong> and <strong>badblocks</strong>.
|
<h3 data-i18n="how_step4_title"></h3>
|
||||||
</p>
|
<p data-i18n="how_step4_text"></p>
|
||||||
|
</article>
|
||||||
<!-- Hero buttons -->
|
|
||||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-6">
|
|
||||||
<a href="/fakecheck/"
|
|
||||||
class="inline-flex items-center justify-center px-6 py-3 rounded-full bg-brand_blue text-off_white text-sm font-semibold shadow-soft hover:bg-brand_blue/90 transition">
|
|
||||||
Start quick check
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="ml-2 h-4 w-4" fill="none"
|
|
||||||
viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
d="M13.5 4.5L21 12l-7.5 7.5M21 12H3"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="#how-it-works"
|
|
||||||
class="inline-flex items-center justify-center px-5 py-3 rounded-full border border-silver text-deep_gray text-sm font-medium hover:border-brand_blue/60 hover:text-brand_blue transition">
|
|
||||||
Learn how it works
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-4 text-xs sm:text-sm text-deep_gray/70">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="inline-flex items-center justify-center w-4 h-4 rounded-full bg-green_check text-off_white text-[10px]">✓</span>
|
|
||||||
<span>Browser-based quick test – nothing installed</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="inline-flex items-center justify-center w-4 h-4 rounded-full bg-green_check text-off_white text-[10px]">✓</span>
|
|
||||||
<span>Pro mode with deep capacity verification</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hero visual -->
|
|
||||||
<div class="flex-1 flex justify-center lg:justify-end">
|
|
||||||
<div class="relative w-full max-w-md">
|
|
||||||
<div class="absolute -inset-4 bg-brand_blue/5 rounded-3xl blur-2xl"></div>
|
|
||||||
<div class="relative bg-off_white rounded-3xl shadow-soft border border-silver/40 p-6">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<div>
|
|
||||||
<p class="heading-font text-sm font-semibold text-deep_gray">
|
|
||||||
Quick USB check
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-deep_gray/70">
|
|
||||||
Example device: 64 GB USB 3.1 flash drive
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span class="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-light_gray text-green_check text-xs font-medium">
|
|
||||||
<span class="w-2 h-2 rounded-full bg-green_check"></span>
|
|
||||||
OK
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fake mini "progress" -->
|
|
||||||
<div class="space-y-3 mb-4">
|
|
||||||
<div>
|
|
||||||
<div class="flex justify-between text-xs text-deep_gray/70 mb-1">
|
|
||||||
<span>Write test</span>
|
|
||||||
<span>145 MB/s</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full h-2 rounded-full bg-light_gray overflow-hidden">
|
|
||||||
<div class="h-2 rounded-full bg-brand_blue w-4/5"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="flex justify-between text-xs text-deep_gray/70 mb-1">
|
|
||||||
<span>Read test</span>
|
|
||||||
<span>180 MB/s</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full h-2 rounded-full bg-light_gray overflow-hidden">
|
|
||||||
<div class="h-2 rounded-full bg-green_check w-11/12"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="flex justify-between text-xs text-deep_gray/70 mb-1">
|
|
||||||
<span>Integrity blocks checked</span>
|
|
||||||
<span>8.0 GB of 8.0 GB</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full h-2 rounded-full bg-light_gray overflow-hidden">
|
|
||||||
<div class="h-2 rounded-full bg-amber_yellow w-full"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between text-xs text-deep_gray/80">
|
|
||||||
<span>Result: drive looks genuine</span>
|
|
||||||
<a href="/fakecheck/"
|
|
||||||
class="inline-flex items-center gap-1 text-brand_blue hover:underline">
|
|
||||||
Re-run test
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- USB image (optional) -->
|
|
||||||
<div class="absolute -bottom-10 -right-6 hidden sm:block">
|
|
||||||
<img src="/img/stick_blank.png"
|
|
||||||
alt="USB stick illustration"
|
|
||||||
class="h-24 drop-shadow-lg">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- HOW IT WORKS -->
|
|
||||||
<section id="how-it-works" class="py-16 bg-off_white border-t border-silver/20">
|
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="max-w-3xl mb-10">
|
|
||||||
<h2 class="heading-font text-2xl sm:text-3xl font-bold mb-3">
|
|
||||||
How FakeUSBCheck works
|
|
||||||
</h2>
|
|
||||||
<p class="text-deep_gray/80 text-sm sm:text-base">
|
|
||||||
FakeUSBCheck combines a safe, browser-based quick check with an optional Pro mode.
|
|
||||||
The browser mode focuses on performance and basic integrity. The Pro mode, supported
|
|
||||||
by a small helper app on your computer, can run deep capacity and surface tests similar
|
|
||||||
to established tools like f3 or badblocks.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-8 md:grid-cols-3">
|
|
||||||
<div class="bg-very_light_gray rounded-2xl p-6 border border-light_gray">
|
|
||||||
<div class="flex items-center justify-center w-10 h-10 rounded-full bg-brand_blue/10 text-brand_blue mb-4">
|
|
||||||
1
|
|
||||||
</div>
|
|
||||||
<h3 class="heading-font text-lg font-semibold mb-2">Select your USB drive</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80">
|
|
||||||
Choose the folder on your USB flash drive you want to test. The browser never
|
|
||||||
sees other drives or folders – you stay in control of what is accessed.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-very_light_gray rounded-2xl p-6 border border-light_gray">
|
|
||||||
<div class="flex items-center justify-center w-10 h-10 rounded-full bg-brand_blue/10 text-brand_blue mb-4">
|
|
||||||
2
|
|
||||||
</div>
|
|
||||||
<h3 class="heading-font text-lg font-semibold mb-2">Run the quick browser check</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80">
|
|
||||||
FakeUSBCheck writes a configurable amount of test data, reads it back
|
|
||||||
and calculates checksums. You see write and read speeds, plus a basic
|
|
||||||
integrity result for the tested area.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-very_light_gray rounded-2xl p-6 border border-light_gray">
|
|
||||||
<div class="flex items-center justify-center w-10 h-10 rounded-full bg-brand_blue/10 text-brand_blue mb-4">
|
|
||||||
3
|
|
||||||
</div>
|
|
||||||
<h3 class="heading-font text-lg font-semibold mb-2">Unlock Pro mode for deep tests</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80">
|
|
||||||
In Pro mode you install a small helper application that can perform full-drive
|
|
||||||
surface scans, fake capacity detection and extended stress tests, while
|
|
||||||
FakeUSBCheck provides the dashboard and reporting.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- FEATURES -->
|
|
||||||
<section id="features" class="py-16 bg-very_light_gray">
|
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="max-w-3xl mb-10">
|
|
||||||
<h2 class="heading-font text-2xl sm:text-3xl font-bold mb-3">
|
|
||||||
Designed for safety, speed and clarity
|
|
||||||
</h2>
|
|
||||||
<p class="text-deep_gray/80 text-sm sm:text-base">
|
|
||||||
FakeUSBCheck focuses on giving you a clear result: is this drive worth trusting?
|
|
||||||
All tests are designed to be understandable, with transparent metrics and
|
|
||||||
optional in-depth reports for advanced users.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-3">
|
|
||||||
<div class="bg-off_white rounded-2xl p-6 border border-light_gray">
|
|
||||||
<h3 class="heading-font text-lg font-semibold mb-2">Quick browser check</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80 mb-3">
|
|
||||||
Start a quick test directly from your browser – no installation required.
|
|
||||||
Ideal before copying important data or using a new drive for the first time.
|
|
||||||
</p>
|
|
||||||
<ul class="text-sm text-deep_gray/80 space-y-1">
|
|
||||||
<li>• Test presets: 200 MB, 2 GB, 8 GB</li>
|
|
||||||
<li>• Write & read throughput in MB/s</li>
|
|
||||||
<li>• Checksum-based integrity verification</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-off_white rounded-2xl p-6 border border-light_gray">
|
|
||||||
<h3 class="heading-font text-lg font-semibold mb-2">Pro capacity verification</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80 mb-3">
|
|
||||||
Detect fake capacity drives that claim unrealistic gigabytes or terabytes.
|
|
||||||
The Pro helper app can run full media scans while the web dashboard
|
|
||||||
visualizes progress and results.
|
|
||||||
</p>
|
|
||||||
<ul class="text-sm text-deep_gray/80 space-y-1">
|
|
||||||
<li>• Detection of over-reported capacity</li>
|
|
||||||
<li>• Optional full surface scan</li>
|
|
||||||
<li>• JSON report & PDF export</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-off_white rounded-2xl p-6 border border-light_gray">
|
|
||||||
<h3 class="heading-font text-lg font-semibold mb-2">Clear reports</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80 mb-3">
|
|
||||||
Every test produces a human-readable summary and a technical report.
|
|
||||||
That makes it easy to document defective devices or prove that a drive
|
|
||||||
behaves as expected.
|
|
||||||
</p>
|
|
||||||
<ul class="text-sm text-deep_gray/80 space-y-1">
|
|
||||||
<li>• Simple “OK / Warning / Failed” status</li>
|
|
||||||
<li>• Detailed logs for advanced users</li>
|
|
||||||
<li>• Optional sharing link for support</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- PRICING -->
|
|
||||||
<section id="pricing" class="py-16 bg-off_white border-t border-silver/20">
|
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="max-w-3xl mb-10 text-center mx-auto">
|
|
||||||
<h2 class="heading-font text-2xl sm:text-3xl font-bold mb-3">
|
|
||||||
Free quick check or Pro deep analysis
|
|
||||||
</h2>
|
|
||||||
<p class="text-deep_gray/80 text-sm sm:text-base">
|
|
||||||
Start with the free browser-based quick check. If you need full-drive verification, repeated
|
|
||||||
test runs or PDF reports, upgrade to Pro. No subscription is required for basic checks.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
|
||||||
<!-- Free -->
|
|
||||||
<div class="bg-very_light_gray rounded-3xl border border-light_gray p-6 flex flex-col">
|
|
||||||
<h3 class="heading-font text-xl font-semibold mb-1">Free quick check</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80 mb-4">
|
|
||||||
Ideal for a fast sanity check of a new USB stick before you trust it with important files.
|
|
||||||
</p>
|
|
||||||
<p class="text-3xl font-bold heading-font mb-4">€0</p>
|
|
||||||
<ul class="text-sm text-deep_gray/80 space-y-2 mb-6">
|
|
||||||
<li>• Browser-based test, no install</li>
|
|
||||||
<li>• Basic write & read speed measurement</li>
|
|
||||||
<li>• Configurable test size (up to a limit)</li>
|
|
||||||
<li>• Simple “OK / Warning / Failed” result</li>
|
|
||||||
</ul>
|
|
||||||
<a href="/fakecheck/"
|
|
||||||
class="mt-auto inline-flex items-center justify-center px-5 py-3 rounded-full bg-brand_blue text-off_white text-sm font-semibold hover:bg-brand_blue/90 transition">
|
|
||||||
Start free quick check
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pro -->
|
|
||||||
<div class="bg-deep_gray rounded-3xl border border-deep_gray text-off_white p-6 flex flex-col relative overflow-hidden">
|
|
||||||
<div class="absolute top-4 right-4 px-3 py-1 rounded-full bg-amber_yellow text-xs font-semibold text-deep_gray">
|
|
||||||
Coming soon
|
|
||||||
</div>
|
|
||||||
<h3 class="heading-font text-xl font-semibold mb-1">Pro deep check</h3>
|
|
||||||
<p class="text-sm text-off_white/80 mb-4">
|
|
||||||
For power users, IT support and anyone buying larger batches of USB drives who needs
|
|
||||||
reproducible, documented test results.
|
|
||||||
</p>
|
|
||||||
<p class="text-3xl font-bold heading-font mb-4">Planned</p>
|
|
||||||
<ul class="text-sm text-off_white/80 space-y-2 mb-6">
|
|
||||||
<li>• Helper app for Windows, macOS & Linux</li>
|
|
||||||
<li>• Full-drive surface tests & capacity verification</li>
|
|
||||||
<li>• Detailed performance statistics and logs</li>
|
|
||||||
<li>• PDF and JSON export for documentation</li>
|
|
||||||
</ul>
|
|
||||||
<button
|
|
||||||
class="mt-auto inline-flex items-center justify-center px-5 py-3 rounded-full bg-off_white text-deep_gray text-sm font-semibold hover:bg-off_white/90 transition">
|
|
||||||
Get notified about Pro launch
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- SECURITY -->
|
|
||||||
<section id="security" class="py-16 bg-very_light_gray">
|
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="grid gap-10 md:grid-cols-2 items-start">
|
|
||||||
<div>
|
|
||||||
<h2 class="heading-font text-2xl sm:text-3xl font-bold mb-3">
|
|
||||||
Built with privacy and data safety in mind
|
|
||||||
</h2>
|
|
||||||
<p class="text-sm sm:text-base text-deep_gray/80 mb-4">
|
|
||||||
Your data on the USB drive is sensitive. FakeUSBCheck is designed so that you stay in
|
|
||||||
control at every step. The browser-based quick check never uploads your test data.
|
|
||||||
Only aggregated test results are sent to our servers – and only if you explicitly agree.
|
|
||||||
</p>
|
|
||||||
<p class="text-sm sm:text-base text-deep_gray/80 mb-4">
|
|
||||||
The optional Pro helper runs locally on your device and only talks to our API using secure,
|
|
||||||
encrypted connections. You can review exactly what is being sent and revoke access at any
|
|
||||||
time.
|
|
||||||
</p>
|
|
||||||
<p class="text-sm sm:text-base text-deep_gray/80">
|
|
||||||
We do not sell your test results to third parties. Anonymous statistics may be used to
|
|
||||||
improve detection of common fake models and to warn users about frequently abused brands.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-off_white rounded-3xl border border-light_gray p-6 shadow-soft">
|
|
||||||
<h3 class="heading-font text-lg font-semibold mb-3">Security highlights</h3>
|
|
||||||
<ul class="text-sm text-deep_gray/85 space-y-2 mb-4">
|
|
||||||
<li>• Browser quick check: test data stays on your device</li>
|
|
||||||
<li>• Optional upload of summarized results only</li>
|
|
||||||
<li>• Pro helper: TLS-encrypted communication</li>
|
|
||||||
<li>• Transparent logs so you can see what happened</li>
|
|
||||||
<li>• No hidden “phone home” behaviour</li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-xs text-deep_gray/65">
|
|
||||||
Detailed privacy and security documentation will be available before the Pro mode
|
|
||||||
leaves beta. Our goal: you should be able to explain to a non-technical friend exactly
|
|
||||||
what FakeUSBCheck does – and what it doesn’t do.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- FAQ -->
|
|
||||||
<section id="faq" class="py-16 bg-off_white border-t border-silver/20">
|
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="max-w-3xl mb-10">
|
|
||||||
<h2 class="heading-font text-2xl sm:text-3xl font-bold mb-3">
|
|
||||||
Frequently asked questions
|
|
||||||
</h2>
|
|
||||||
<p class="text-deep_gray/80 text-sm sm:text-base">
|
|
||||||
A few common questions about testing USB flash drives and how FakeUSBCheck fits into
|
|
||||||
your workflow.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div class="bg-very_light_gray rounded-2xl p-5 border border-light_gray">
|
|
||||||
<h3 class="heading-font text-base font-semibold mb-2">
|
|
||||||
Can FakeUSBCheck damage my USB drive?
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80">
|
|
||||||
Any write test puts some wear on flash memory. The quick check is designed to use a
|
|
||||||
reasonable test size so that the wear stays minimal for healthy drives. The Pro mode
|
|
||||||
can perform heavy tests; those are intended for situations where you specifically want
|
|
||||||
to stress-test a drive or check a suspicious device.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-very_light_gray rounded-2xl p-5 border border-light_gray">
|
|
||||||
<h3 class="heading-font text-base font-semibold mb-2">
|
|
||||||
Do I have to install anything for the free quick check?
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80">
|
|
||||||
No. The browser-based quick check runs entirely inside your browser, using modern
|
|
||||||
APIs that allow you to work with files and folders on your USB drive with your
|
|
||||||
explicit consent. You only need to install the Pro helper if you want deep,
|
|
||||||
full-drive tests.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-very_light_gray rounded-2xl p-5 border border-light_gray">
|
|
||||||
<h3 class="heading-font text-base font-semibold mb-2">
|
|
||||||
How reliable is the detection of fake capacity drives?
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80">
|
|
||||||
Detecting fake capacities requires writing and reading large portions of the drive.
|
|
||||||
The Pro mode is specifically designed for this type of validation. While no tool can
|
|
||||||
guarantee 100% detection of all possible manipulation techniques, deep surface tests
|
|
||||||
provide strong evidence and clear documentation when a drive behaves incorrectly.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-very_light_gray rounded-2xl p-5 border border-light_gray">
|
|
||||||
<h3 class="heading-font text-base font-semibold mb-2">
|
|
||||||
Can I test external SSDs or only USB sticks?
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm text-deep_gray/80">
|
|
||||||
The quick browser check focuses on USB flash drives, but in practice it works with
|
|
||||||
any storage that appears as a normal folder, including some external SSDs. The Pro
|
|
||||||
helper can work with a broader range of devices, including USB-connected SSDs and
|
|
||||||
some memory cards, depending on your operating system.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- FINAL CTA -->
|
|
||||||
<section class="py-16 bg-very_light_gray border-t border-silver/20">
|
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="bg-deep_gray rounded-3xl px-6 py-8 sm:px-10 sm:py-10 text-off_white flex flex-col md:flex-row items-start md:items-center justify-between gap-6 shadow-soft">
|
|
||||||
<div>
|
|
||||||
<h2 class="heading-font text-2xl sm:text-3xl font-bold mb-3">
|
|
||||||
Ready to see if your USB sticks are real?
|
|
||||||
</h2>
|
|
||||||
<p class="text-sm sm:text-base text-off_white/80">
|
|
||||||
Start a free quick check in your browser. No account, no installation. If you need
|
|
||||||
more, Pro mode will be ready soon – with full-drive tests and exportable reports.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col sm:flex-row gap-3 min-w-[200px]">
|
|
||||||
<a href="/fakecheck/"
|
|
||||||
class="inline-flex items-center justify-center px-6 py-3 rounded-full bg-off_white text-deep_gray text-sm font-semibold hover:bg-off_white/90 transition">
|
|
||||||
Start free quick check
|
|
||||||
</a>
|
|
||||||
<a href="#pricing"
|
|
||||||
class="inline-flex items-center justify-center px-6 py-3 rounded-full border border-off_white/60 text-off_white text-sm font-semibold hover:bg-off_white/10 transition">
|
|
||||||
View features
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="bg-deep_gray text-off_white py-8">
|
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row gap-4 sm:items-center sm:justify-between">
|
|
||||||
<p class="text-xs text-off_white/70">
|
|
||||||
© <!--?php echo date('Y'); ?--> FakeUSBCheck. All rights reserved.
|
|
||||||
</p>
|
|
||||||
<div class="flex flex-wrap gap-4 text-xs text-off_white/70">
|
|
||||||
<a href="/imprint" class="hover:text-off_white">Imprint</a>
|
|
||||||
<a href="/privacy" class="hover:text-off_white">Privacy</a>
|
|
||||||
<a href="/terms" class="hover:text-off_white">Terms</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Simple JS hook for login/avatar state -->
|
<!-- FEATURES -->
|
||||||
<script>
|
<section class="section section-alt" id="features">
|
||||||
// Example: global helper functions you can call from your real auth code
|
<div class="container">
|
||||||
window.fakeUsbAuth = {
|
<h2 class="section-title" data-i18n="features_title"></h2>
|
||||||
setLoggedIn(displayName) {
|
<p class="section-lead" data-i18n="features_intro"></p>
|
||||||
const loginBtn = document.getElementById('loginButton');
|
|
||||||
const loginIconMobile = document.getElementById('loginIconMobile');
|
|
||||||
const userAvatar = document.getElementById('userAvatar');
|
|
||||||
const avatarText = document.getElementById('userAvatarInitials');
|
|
||||||
|
|
||||||
if (displayName && avatarText) {
|
<div class="features-grid">
|
||||||
const parts = displayName.trim().split(' ');
|
<article class="feature-card">
|
||||||
const initials = parts.length >= 2
|
<h3 data-i18n="feature_free_title"></h3>
|
||||||
? (parts[0][0] + parts[1][0])
|
<ul>
|
||||||
: (displayName[0] || 'U');
|
<li data-i18n="feature_free_1"></li>
|
||||||
avatarText.textContent = initials.toUpperCase();
|
<li data-i18n="feature_free_2"></li>
|
||||||
}
|
<li data-i18n="feature_free_3"></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
if (loginBtn) loginBtn.classList.add('hidden');
|
<article class="feature-card feature-card-pro">
|
||||||
if (loginIconMobile) loginIconMobile.classList.add('hidden');
|
<div class="pill" data-i18n="label_coming_soon"></div>
|
||||||
if (userAvatar) userAvatar.classList.remove('hidden');
|
<h3 data-i18n="feature_pro_title"></h3>
|
||||||
},
|
<ul>
|
||||||
setLoggedOut() {
|
<li data-i18n="feature_pro_1"></li>
|
||||||
const loginBtn = document.getElementById('loginButton');
|
<li data-i18n="feature_pro_2"></li>
|
||||||
const loginIconMobile = document.getElementById('loginIconMobile');
|
<li data-i18n="feature_pro_3"></li>
|
||||||
const userAvatar = document.getElementById('userAvatar');
|
</ul>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
if (loginBtn) loginBtn.classList.remove('hidden');
|
<!-- PRICING -->
|
||||||
if (loginIconMobile) loginIconMobile.classList.remove('hidden');
|
<section class="section" id="pricing">
|
||||||
if (userAvatar) userAvatar.classList.add('hidden');
|
<div class="container">
|
||||||
}
|
<h2 class="section-title" data-i18n="pricing_title"></h2>
|
||||||
};
|
<p class="section-lead" data-i18n="pricing_intro"></p>
|
||||||
|
|
||||||
// Initial state: logged out
|
<div class="pricing-grid">
|
||||||
window.fakeUsbAuth.setLoggedOut();
|
<article class="pricing-card">
|
||||||
</script>
|
<h3 data-i18n="pricing_free_title"></h3>
|
||||||
|
<p class="price-tag" data-i18n="pricing_free_price"></p>
|
||||||
|
<ul>
|
||||||
|
<li data-i18n="pricing_free_1"></li>
|
||||||
|
<li data-i18n="pricing_free_2"></li>
|
||||||
|
<li data-i18n="pricing_free_3"></li>
|
||||||
|
</ul>
|
||||||
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/fakecheck/', $currentLang)); ?>"
|
||||||
|
class="btn btn-primary"
|
||||||
|
data-i18n="pricing_free_cta"></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="pricing-card pricing-card-muted">
|
||||||
|
<h3 data-i18n="pricing_pro_title"></h3>
|
||||||
|
<p class="price-tag" data-i18n="pricing_pro_price"></p>
|
||||||
|
<ul>
|
||||||
|
<li data-i18n="pricing_pro_1"></li>
|
||||||
|
<li data-i18n="pricing_pro_2"></li>
|
||||||
|
<li data-i18n="pricing_pro_3"></li>
|
||||||
|
</ul>
|
||||||
|
<button class="btn btn-disabled" disabled data-i18n="pricing_pro_cta"></button>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FAQ -->
|
||||||
|
<section class="section section-alt" id="faq">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title" data-i18n="faq_title"></h2>
|
||||||
|
<p class="section-lead" data-i18n="faq_intro"></p>
|
||||||
|
|
||||||
|
<div class="faq-grid">
|
||||||
|
<details class="faq-item" open>
|
||||||
|
<summary data-i18n="faq_q1"></summary>
|
||||||
|
<p data-i18n="faq_a1"></p>
|
||||||
|
</details>
|
||||||
|
<details class="faq-item">
|
||||||
|
<summary data-i18n="faq_q2"></summary>
|
||||||
|
<p data-i18n="faq_a2"></p>
|
||||||
|
</details>
|
||||||
|
<details class="faq-item">
|
||||||
|
<summary data-i18n="faq_q3"></summary>
|
||||||
|
<p data-i18n="faq_a3"></p>
|
||||||
|
</details>
|
||||||
|
<details class="faq-item">
|
||||||
|
<summary data-i18n="faq_q4"></summary>
|
||||||
|
<p data-i18n="faq_a4"></p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container footer-inner">
|
||||||
|
<p>© <?php echo date('Y'); ?> usbcheck.it</p>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/impressum', $currentLang)); ?>"
|
||||||
|
data-i18n="footer_imprint"></a>
|
||||||
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/datenschutz', $currentLang)); ?>"
|
||||||
|
data-i18n="footer_privacy"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/lang.js?v=1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
82
public/login.php
Normal file
82
public/login.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../config/db.php';
|
||||||
|
require __DIR__ . '/../src/Auth.php';
|
||||||
|
|
||||||
|
$auth = new Auth($pdo);
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$identifier = trim($_POST['identifier'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
if ($auth->login($identifier, $password)) {
|
||||||
|
header('Location: /'); // nach Login auf Startseite
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = 'Login fehlgeschlagen. Bitte Zugangsdaten prüfen.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Login – usbcheck.it</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<!-- Dein Tailwind CSS -->
|
||||||
|
<link rel="stylesheet" href="/css/tailwind.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-[#FAFAFA] text-[#1A1A1A] font-[Inter]">
|
||||||
|
<div class="min-h-screen flex items-center justify-center px-4">
|
||||||
|
<div class="w-full max-w-md bg-white shadow-lg rounded-2xl p-8">
|
||||||
|
<h1 class="text-2xl font-[Montserrat] font-bold mb-6 text-center">
|
||||||
|
Anmelden bei <span class="text-[#0051FF]">usbcheck.it</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="mb-4 text-sm text-[#E63946]">
|
||||||
|
<?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1" for="identifier">
|
||||||
|
E-Mail oder Benutzername
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="identifier"
|
||||||
|
name="identifier"
|
||||||
|
required
|
||||||
|
class="w-full border border-[#C8CBD0] rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#0051FF]"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1" for="password">
|
||||||
|
Passwort
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
class="w-full border border-[#C8CBD0] rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#0051FF]"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full bg-[#0051FF] text-white font-[Montserrat] font-semibold rounded-lg py-2 mt-4 hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
public/logout.php
Normal file
11
public/logout.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../config/db.php';
|
||||||
|
require __DIR__ . '/../src/Auth.php';
|
||||||
|
|
||||||
|
$auth = new Auth($pdo);
|
||||||
|
$auth->logout();
|
||||||
|
|
||||||
|
header('Location: /');
|
||||||
|
exit;
|
||||||
75
public/partial/header.php
Normal file
75
public/partial/header.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
// public/partials/header.php
|
||||||
|
|
||||||
|
// Fallback, falls $currentLang nicht gesetzt ist
|
||||||
|
if (!isset($currentLang)) {
|
||||||
|
$currentLang = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
$supportedLangs = [
|
||||||
|
'de' => 'Deutsch',
|
||||||
|
'en' => 'English',
|
||||||
|
'it' => 'Italiano',
|
||||||
|
'fr' => 'Français',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut eine URL und hängt immer ?lang=<code> dran.
|
||||||
|
* $path sollte mit / beginnen, z.B. "/", "/fakecheck/", "/impressum".
|
||||||
|
*/
|
||||||
|
function usbcheck_url_with_lang(string $path, string $lang): string
|
||||||
|
{
|
||||||
|
$path = $path ?: '/';
|
||||||
|
$separator = str_contains($path, '?') ? '&' : '?';
|
||||||
|
return $path . $separator . 'lang=' . urlencode($lang);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="container header-inner">
|
||||||
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/', $currentLang)); ?>" class="logo-wrap">
|
||||||
|
<img src="/img/logo.png" alt="usbcheck.it Logo" class="logo-img">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav class="main-nav">
|
||||||
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/', $currentLang)); ?>#how-it-works"
|
||||||
|
class="nav-link"
|
||||||
|
data-i18n="nav_how_it_works"></a>
|
||||||
|
|
||||||
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/', $currentLang)); ?>#features"
|
||||||
|
class="nav-link"
|
||||||
|
data-i18n="nav_features"></a>
|
||||||
|
|
||||||
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/', $currentLang)); ?>#pricing"
|
||||||
|
class="nav-link"
|
||||||
|
data-i18n="nav_pricing"></a>
|
||||||
|
|
||||||
|
<a href="<?php echo htmlspecialchars(usbcheck_url_with_lang('/', $currentLang)); ?>#faq"
|
||||||
|
class="nav-link"
|
||||||
|
data-i18n="nav_faq"></a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="lang-switch" data-current-lang="<?php echo htmlspecialchars($currentLang); ?>">
|
||||||
|
<button id="lang-current" class="lang-current">
|
||||||
|
<?php echo strtoupper(htmlspecialchars($currentLang)); ?>
|
||||||
|
</button>
|
||||||
|
<div id="lang-menu" class="lang-menu hidden">
|
||||||
|
<?php foreach ($supportedLangs as $code => $label): ?>
|
||||||
|
<button data-lang="<?php echo htmlspecialchars($code); ?>">
|
||||||
|
<?php echo htmlspecialchars($label); ?>
|
||||||
|
</button>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="login-button"
|
||||||
|
class="btn btn-outline"
|
||||||
|
data-i18n="btn_login"></button>
|
||||||
|
|
||||||
|
<div id="user-avatar" class="user-avatar hidden" title="Account">
|
||||||
|
<!-- Standard: Initialen, später durch Bild ersetzbar -->
|
||||||
|
<span id="user-avatar-initials">U</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
198
public/register.php
Normal file
198
public/register.php
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<?php
|
||||||
|
// public/register.php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/auth.php';
|
||||||
|
|
||||||
|
$lang = auth_get_lang();
|
||||||
|
$csrfToken = auth_csrf_token();
|
||||||
|
$currentUser = auth_current_user();
|
||||||
|
if ($currentUser) {
|
||||||
|
// Bereits eingeloggt -> Accountseite
|
||||||
|
header('Location: /account.php?lang=' . urlencode($lang));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$values = [
|
||||||
|
'email' => '',
|
||||||
|
'username' => '',
|
||||||
|
'full_name' => '',
|
||||||
|
'preferred_lang' => $lang,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (!auth_verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||||
|
$errors['csrf'] = 'Deine Sitzung ist abgelaufen. Bitte Seite neu laden.';
|
||||||
|
} else {
|
||||||
|
$email = $_POST['email'] ?? '';
|
||||||
|
$username = $_POST['username'] ?? '';
|
||||||
|
$fullName = $_POST['full_name'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$passwordConfirm= $_POST['password_confirm'] ?? '';
|
||||||
|
$preferredLang = $_POST['preferred_lang'] ?? $lang;
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'email' => $email,
|
||||||
|
'username' => $username,
|
||||||
|
'full_name' => $fullName,
|
||||||
|
'preferred_lang' => $preferredLang,
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = auth_register_user(
|
||||||
|
$email,
|
||||||
|
$username,
|
||||||
|
$fullName,
|
||||||
|
$password,
|
||||||
|
$passwordConfirm,
|
||||||
|
$preferredLang
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
header('Location: /account.php?lang=' . urlencode($preferredLang));
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$errors = array_merge($errors, $result['errors']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<?php echo htmlspecialchars($lang, ENT_QUOTES); ?>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Registrierung – USBCheck</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Montserrat:wght@600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Main CSS -->
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css?v=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php
|
||||||
|
// Header-Partial, erwartet ggf. $lang
|
||||||
|
$langVar = $lang;
|
||||||
|
include __DIR__ . '/partials/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="page-main">
|
||||||
|
<section class="section">
|
||||||
|
<div class="container narrow">
|
||||||
|
<h1 class="section-title" data-i18n="register_title">Konto erstellen</h1>
|
||||||
|
<p class="section-lead" data-i18n="register_intro">
|
||||||
|
Erstelle ein kostenloses Konto, um deine USB-Tests zu verwalten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if (!empty($errors['csrf'])): ?>
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<?php echo htmlspecialchars($errors['csrf'], ENT_QUOTES); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form class="form-card" method="post" action="/register.php?lang=<?php echo urlencode($lang); ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken, ENT_QUOTES); ?>">
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="email" data-i18n="register_email_label">E-Mail-Adresse</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
value="<?php echo htmlspecialchars($values['email'], ENT_QUOTES); ?>"
|
||||||
|
>
|
||||||
|
<?php if (!empty($errors['email'])): ?>
|
||||||
|
<p class="form-error"><?php echo htmlspecialchars($errors['email'], ENT_QUOTES); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="username" data-i18n="register_username_label">Username</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
value="<?php echo htmlspecialchars($values['username'], ENT_QUOTES); ?>"
|
||||||
|
>
|
||||||
|
<?php if (!empty($errors['username'])): ?>
|
||||||
|
<p class="form-error"><?php echo htmlspecialchars($errors['username'], ENT_QUOTES); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="full_name" data-i18n="register_full_name_label">Vollständiger Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="full_name"
|
||||||
|
name="full_name"
|
||||||
|
required
|
||||||
|
value="<?php echo htmlspecialchars($values['full_name'], ENT_QUOTES); ?>"
|
||||||
|
>
|
||||||
|
<?php if (!empty($errors['full_name'])): ?>
|
||||||
|
<p class="form-error"><?php echo htmlspecialchars($errors['full_name'], ENT_QUOTES); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row form-row-inline">
|
||||||
|
<div>
|
||||||
|
<label for="password" data-i18n="register_password_label">Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
minlength="10"
|
||||||
|
>
|
||||||
|
<?php if (!empty($errors['password'])): ?>
|
||||||
|
<p class="form-error"><?php echo htmlspecialchars($errors['password'], ENT_QUOTES); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password_confirm" data-i18n="register_password_confirm_label">Passwort wiederholen</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password_confirm"
|
||||||
|
name="password_confirm"
|
||||||
|
required
|
||||||
|
minlength="10"
|
||||||
|
>
|
||||||
|
<?php if (!empty($errors['password_confirm'])): ?>
|
||||||
|
<p class="form-error"><?php echo htmlspecialchars($errors['password_confirm'], ENT_QUOTES); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="preferred_lang" data-i18n="register_lang_label">Bevorzugte Sprache</label>
|
||||||
|
<select id="preferred_lang" name="preferred_lang">
|
||||||
|
<option value="de" <?php echo $values['preferred_lang'] === 'de' ? 'selected' : ''; ?>>Deutsch</option>
|
||||||
|
<option value="en" <?php echo $values['preferred_lang'] === 'en' ? 'selected' : ''; ?>>English</option>
|
||||||
|
<option value="it" <?php echo $values['preferred_lang'] === 'it' ? 'selected' : ''; ?>>Italiano</option>
|
||||||
|
<option value="fr" <?php echo $values['preferred_lang'] === 'fr' ? 'selected' : ''; ?>>Français</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" data-i18n="register_submit">
|
||||||
|
Konto erstellen
|
||||||
|
</button>
|
||||||
|
<p class="form-hint">
|
||||||
|
<span data-i18n="register_existing_hint">Du hast bereits ein Konto?</span>
|
||||||
|
<a href="/login.php?lang=<?php echo urlencode($lang); ?>" data-i18n="register_existing_link">
|
||||||
|
Zum Login
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/assets/js/lang.js?v=1"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
public/uploads/avatar/.gitkeep
Normal file
0
public/uploads/avatar/.gitkeep
Normal file
111
sql.schema
Normal file
111
sql.schema
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- USERS – Benutzerkonto + spätere Rechnungs-/Zahlungsinfos
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Login
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(150) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
avatar_path VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
-- Persönliche Daten
|
||||||
|
first_name VARCHAR(100) NULL,
|
||||||
|
last_name VARCHAR(100) NULL,
|
||||||
|
|
||||||
|
-- spätere Rechnungsdaten / Billing
|
||||||
|
company_name VARCHAR(255) NULL,
|
||||||
|
street VARCHAR(255) NULL,
|
||||||
|
postal_code VARCHAR(20) NULL,
|
||||||
|
city VARCHAR(255) NULL,
|
||||||
|
country VARCHAR(100) NULL,
|
||||||
|
vat_id VARCHAR(50) NULL,
|
||||||
|
|
||||||
|
-- spätere Pro-Features / Limits
|
||||||
|
plan ENUM('free', 'pro', 'enterprise') DEFAULT 'free',
|
||||||
|
plan_valid_until DATETIME NULL,
|
||||||
|
|
||||||
|
-- Sicherheit
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
last_login_at DATETIME NULL,
|
||||||
|
failed_logins INT DEFAULT 0,
|
||||||
|
is_locked TINYINT(1) DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- USB DEVICES – vom Nutzer gespeicherte USB-Sticks
|
||||||
|
-- Ein Benutzer kann mehrere Sticks speichern.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS usb_devices (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
serial_number VARCHAR(255) NULL,
|
||||||
|
manufacturer VARCHAR(255) NULL,
|
||||||
|
model_name VARCHAR(255) NULL,
|
||||||
|
usb_type ENUM('USB 2.0', 'USB 3.0', 'USB 3.1', 'USB 3.2', 'USB 4.0') NULL,
|
||||||
|
|
||||||
|
capacity_bytes BIGINT UNSIGNED NULL,
|
||||||
|
advertised_capacity_bytes BIGINT UNSIGNED NULL,
|
||||||
|
|
||||||
|
read_speed_mbps FLOAT NULL,
|
||||||
|
write_speed_mbps FLOAT NULL,
|
||||||
|
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- USB TEST RESULTS – Schnelltest + Pro-Test
|
||||||
|
-- Jedes Testergebnis gehört zu einem Stick.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS usb_tests (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
usb_device_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
-- Testtyp
|
||||||
|
test_type ENUM('quick', 'standard', 'deep', 'pro') NOT NULL,
|
||||||
|
|
||||||
|
-- Ergebniswerte
|
||||||
|
test_start DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
test_end DATETIME NULL,
|
||||||
|
|
||||||
|
read_speed_mbps FLOAT NULL,
|
||||||
|
write_speed_mbps FLOAT NULL,
|
||||||
|
|
||||||
|
integrity_ok TINYINT(1) NULL,
|
||||||
|
checksum_sha256 VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
-- Pro-Modus Zusatzwerte (f3, badblocks etc.)
|
||||||
|
f3_status ENUM('unknown', 'pass', 'fail', 'warning') DEFAULT 'unknown',
|
||||||
|
f3_real_capacity_bytes BIGINT UNSIGNED NULL,
|
||||||
|
f3_lost_bytes BIGINT UNSIGNED NULL,
|
||||||
|
|
||||||
|
badblocks_errors INT NULL,
|
||||||
|
|
||||||
|
-- Metadaten
|
||||||
|
test_report_json JSON NULL,
|
||||||
|
ip_address VARCHAR(45) NULL, -- ipv6 kompatibel
|
||||||
|
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (usb_device_id) REFERENCES usb_devices(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
284
src/Auth.php
Normal file
284
src/Auth.php
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
<?php
|
||||||
|
// src/auth.php
|
||||||
|
// Zentrale Auth-Logik: Session, CSRF, Login, Registrierung, aktueller Nutzer
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config/db.php'; // Stellt $pdo (PDO) bereit
|
||||||
|
|
||||||
|
// --- Session Setup ---
|
||||||
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => '', // Standard: aktuelle Domain
|
||||||
|
'secure' => $secure,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sprache ermitteln / speichern ---
|
||||||
|
function auth_get_lang(): string {
|
||||||
|
if (!empty($_GET['lang'])) {
|
||||||
|
$_SESSION['lang'] = $_GET['lang'];
|
||||||
|
}
|
||||||
|
if (!empty($_SESSION['lang'])) {
|
||||||
|
return $_SESSION['lang'];
|
||||||
|
}
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CSRF-Token ---
|
||||||
|
function auth_csrf_token(): string {
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function auth_verify_csrf(?string $token): bool {
|
||||||
|
if (empty($token) || empty($_SESSION['csrf_token'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return hash_equals($_SESSION['csrf_token'], $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PDO Helper ---
|
||||||
|
function auth_pdo(): PDO {
|
||||||
|
// $pdo kommt aus config/db.php
|
||||||
|
global $pdo;
|
||||||
|
if (!$pdo instanceof PDO) {
|
||||||
|
throw new RuntimeException('Database connection not available.');
|
||||||
|
}
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Aktueller User ---
|
||||||
|
function auth_current_user(): ?array {
|
||||||
|
if (!empty($_SESSION['user_cache']) && is_array($_SESSION['user_cache'])) {
|
||||||
|
return $_SESSION['user_cache'];
|
||||||
|
}
|
||||||
|
if (empty($_SESSION['user_id'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$pdo = auth_pdo();
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id LIMIT 1');
|
||||||
|
$stmt->execute([':id' => $_SESSION['user_id']]);
|
||||||
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$_SESSION['user_cache'] = $user;
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
function auth_require_login(): void {
|
||||||
|
if (!auth_current_user()) {
|
||||||
|
$lang = auth_get_lang();
|
||||||
|
header('Location: /login.php?lang=' . urlencode($lang));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Avatar-Helfer ---
|
||||||
|
function auth_user_initials(array $user): string {
|
||||||
|
$name = $user['full_name'] ?? '';
|
||||||
|
if (trim($name) === '') {
|
||||||
|
$name = $user['username'] ?? $user['email'] ?? 'U';
|
||||||
|
}
|
||||||
|
$parts = preg_split('/\s+/', trim($name));
|
||||||
|
$initials = strtoupper(mb_substr($parts[0], 0, 1));
|
||||||
|
if (count($parts) > 1) {
|
||||||
|
$initials .= strtoupper(mb_substr(end($parts), 0, 1));
|
||||||
|
}
|
||||||
|
return $initials;
|
||||||
|
}
|
||||||
|
|
||||||
|
function auth_user_avatar_url(array $user): ?string {
|
||||||
|
if (!empty($user['avatar_path'])) {
|
||||||
|
return '/uploads/avatars/' . ltrim($user['avatar_path'], '/');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Registrierung ---
|
||||||
|
function auth_register_user(
|
||||||
|
string $email,
|
||||||
|
string $username,
|
||||||
|
string $fullName,
|
||||||
|
string $password,
|
||||||
|
string $passwordConfirm,
|
||||||
|
string $preferredLang
|
||||||
|
): array {
|
||||||
|
$pdo = auth_pdo();
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
$email = trim($email);
|
||||||
|
$username = trim($username);
|
||||||
|
$fullName = trim($fullName);
|
||||||
|
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$errors['email'] = 'Bitte eine gültige E-Mail-Adresse eingeben.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($username === '' || !preg_match('/^[a-zA-Z0-9_.-]{3,32}$/', $username)) {
|
||||||
|
$errors['username'] = 'Username muss 3–32 Zeichen lang sein und darf nur Buchstaben, Zahlen, ., _, - enthalten.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strlen($fullName) < 3) {
|
||||||
|
$errors['full_name'] = 'Bitte einen vollständigen Namen angeben.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strlen($password) < 10) {
|
||||||
|
$errors['password'] = 'Passwort muss mindestens 10 Zeichen lang sein.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($password !== $passwordConfirm) {
|
||||||
|
$errors['password_confirm'] = 'Passwörter stimmen nicht überein.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedLangs = ['de', 'en', 'it', 'fr'];
|
||||||
|
if (!in_array($preferredLang, $allowedLangs, true)) {
|
||||||
|
$preferredLang = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail / Username bereits vergeben?
|
||||||
|
if (!$errors) {
|
||||||
|
$stmt = $pdo->prepare('SELECT email, username FROM users WHERE email = :email OR username = :username LIMIT 1');
|
||||||
|
$stmt->execute([
|
||||||
|
':email' => $email,
|
||||||
|
':username' => $username,
|
||||||
|
]);
|
||||||
|
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($existing) {
|
||||||
|
if (strcasecmp($existing['email'], $email) === 0) {
|
||||||
|
$errors['email'] = 'Diese E-Mail-Adresse wird bereits verwendet.';
|
||||||
|
}
|
||||||
|
if (strcasecmp($existing['username'], $username) === 0) {
|
||||||
|
$errors['username'] = 'Dieser Username ist bereits vergeben.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors) {
|
||||||
|
return ['success' => false, 'errors' => $errors];
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$now = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
INSERT INTO users (email, username, full_name, password_hash, preferred_lang, created_at, updated_at)
|
||||||
|
VALUES (:email, :username, :full_name, :password_hash, :preferred_lang, :created_at, :updated_at)
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
':email' => $email,
|
||||||
|
':username' => $username,
|
||||||
|
':full_name' => $fullName,
|
||||||
|
':password_hash' => $hash,
|
||||||
|
':preferred_lang'=> $preferredLang,
|
||||||
|
':created_at' => $now,
|
||||||
|
':updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$userId = (int)$pdo->lastInsertId();
|
||||||
|
$_SESSION['user_id'] = $userId;
|
||||||
|
unset($_SESSION['user_cache']); // neu laden beim nächsten Zugriff
|
||||||
|
$_SESSION['lang'] = $preferredLang;
|
||||||
|
|
||||||
|
return ['success' => true, 'errors' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Login ---
|
||||||
|
function auth_login(string $login, string $password): array {
|
||||||
|
$pdo = auth_pdo();
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
$login = trim($login);
|
||||||
|
|
||||||
|
if ($login === '' || $password === '') {
|
||||||
|
$errors['login'] = 'Bitte Zugangsdaten vollständig ausfüllen.';
|
||||||
|
return ['success' => false, 'errors' => $errors];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT * FROM users
|
||||||
|
WHERE email = :login OR username = :login
|
||||||
|
LIMIT 1
|
||||||
|
');
|
||||||
|
$stmt->execute([':login' => $login]);
|
||||||
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||||
|
$errors['login'] = 'E-Mail/Username oder Passwort ist falsch.';
|
||||||
|
return ['success' => false, 'errors' => $errors];
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['user_id'] = (int)$user['id'];
|
||||||
|
unset($_SESSION['user_cache']);
|
||||||
|
if (!empty($user['preferred_lang'])) {
|
||||||
|
$_SESSION['lang'] = $user['preferred_lang'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option: Password-Rehash, wenn Algorithmus veraltet
|
||||||
|
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
|
||||||
|
$newHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$upd = $pdo->prepare('UPDATE users SET password_hash = :hash WHERE id = :id');
|
||||||
|
$upd->execute([':hash' => $newHash, ':id' => $user['id']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true, 'errors' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Profil aktualisieren (Name, Sprache) ---
|
||||||
|
function auth_update_profile(int $userId, string $fullName, string $preferredLang): array {
|
||||||
|
$pdo = auth_pdo();
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
$fullName = trim($fullName);
|
||||||
|
if (mb_strlen($fullName) < 3) {
|
||||||
|
$errors['full_name'] = 'Bitte einen gültigen Namen angeben.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedLangs = ['de', 'en', 'it', 'fr'];
|
||||||
|
if (!in_array($preferredLang, $allowedLangs, true)) {
|
||||||
|
$preferredLang = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors) {
|
||||||
|
return ['success' => false, 'errors' => $errors];
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
UPDATE users
|
||||||
|
SET full_name = :full_name,
|
||||||
|
preferred_lang = :preferred_lang,
|
||||||
|
updated_at = :updated_at
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
':full_name' => $fullName,
|
||||||
|
':preferred_lang'=> $preferredLang,
|
||||||
|
':updated_at' => $now,
|
||||||
|
':id' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
unset($_SESSION['user_cache']);
|
||||||
|
$_SESSION['lang'] = $preferredLang;
|
||||||
|
|
||||||
|
return ['success' => true, 'errors' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Logout ---
|
||||||
|
function auth_logout(): void {
|
||||||
|
$_SESSION = [];
|
||||||
|
if (ini_get('session.use_cookies')) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
25
src/Database.php
Normal file
25
src/Database.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
// src/Database.php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
class Database
|
||||||
|
{
|
||||||
|
private static ?PDO $pdo = null;
|
||||||
|
|
||||||
|
public static function getConnection(): PDO
|
||||||
|
{
|
||||||
|
if (self::$pdo === null) {
|
||||||
|
$config = require __DIR__ . '/../config/database.php';
|
||||||
|
|
||||||
|
self::$pdo = new PDO(
|
||||||
|
$config['dsn'],
|
||||||
|
$config['user'],
|
||||||
|
$config['password'],
|
||||||
|
$config['options']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$pdo;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/Session.php
Normal file
87
src/Session.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
// src/Session.php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
class Session
|
||||||
|
{
|
||||||
|
public static function start(): void
|
||||||
|
{
|
||||||
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
// Etwas härtere Session-Cookies
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => isset($_SERVER['HTTPS']),
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function regenerate(): void
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
session_regenerate_id(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function set(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
$_SESSION[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $_SESSION[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function remove(string $key): void
|
||||||
|
{
|
||||||
|
unset($_SESSION[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function destroy(): void
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
$_SESSION = [];
|
||||||
|
if (ini_get("session.use_cookies")) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(
|
||||||
|
session_name(),
|
||||||
|
'',
|
||||||
|
time() - 42000,
|
||||||
|
$params["path"],
|
||||||
|
$params["domain"],
|
||||||
|
$params["secure"],
|
||||||
|
$params["httponly"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function csrfToken(): string
|
||||||
|
{
|
||||||
|
self::start();
|
||||||
|
if (!isset($_SESSION['_csrf_token'])) {
|
||||||
|
$_SESSION['_csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['_csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validateCsrf(?string $token): bool
|
||||||
|
{
|
||||||
|
self::start();
|
||||||
|
if (!isset($_SESSION['_csrf_token']) || !$token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$valid = hash_equals($_SESSION['_csrf_token'], $token);
|
||||||
|
if ($valid) {
|
||||||
|
// Optional: Token nach Benutzung rotieren
|
||||||
|
unset($_SESSION['_csrf_token']);
|
||||||
|
}
|
||||||
|
return $valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user