Add Privacy Policy and Terms of Service pages

Registration collects health-adjacent data (age, weight, height, health
notes, goals) with no privacy policy or ToS anywhere in the app. Adds
both as real pages at /privacy and /terms, a site-wide footer linking to
them (the app had no footer at all outside the homepage), and a consent
line on the registration form. Both pages are marked as a starting draft
with bracketed placeholders for operator/jurisdiction details rather
than inventing legal claims the app can't back up.

Also removed the homepage's own footer, which duplicated the new global
one and whose Privacy/Terms/About/Contact links were all dead `#`
placeholders, and fixed APP_NAME (was "Laravel", site is branded
FitAtlas everywhere else including the browser tab title).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 11:55:11 +03:00
co-authored by Claude Sonnet 5
parent 0a36afbfdc
commit 0c6a297198
11 changed files with 292 additions and 39 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
APP_NAME=Laravel APP_NAME=FitAtlas
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers;
use Illuminate\View\View;
class LegalController extends Controller
{
public function privacy(): View
{
return view('legal.privacy');
}
public function terms(): View
{
return view('legal.terms');
}
}
+7 -6
View File
@@ -127,12 +127,6 @@ return [
'q6' => 'What languages are supported?', 'q6' => 'What languages are supported?',
'a6' => 'English and Romanian. Switch with the language control in the header.', 'a6' => 'English and Romanian. Switch with the language control in the header.',
], ],
'footer' => [
'about' => 'About',
'privacy' => 'Privacy',
'terms' => 'Terms',
'contact' => 'Contact',
],
], ],
'workouts' => [ 'workouts' => [
@@ -252,7 +246,12 @@ return [
'recent_workouts' => 'Recent workouts', 'recent_workouts' => 'Recent workouts',
'none' => 'Nothing to show yet.', 'none' => 'Nothing to show yet.',
'manage_workouts' => 'Manage workouts', 'manage_workouts' => 'Manage workouts',
],
'footer' => [
'privacy' => 'Privacy Policy',
'terms' => 'Terms of Service',
'rights' => 'All rights reserved.',
], ],
'yoga' => [ 'yoga' => [
@@ -316,6 +315,8 @@ return [
'back' => 'Back', 'back' => 'Back',
'register' => 'Register', 'register' => 'Register',
], ],
'consent_prefix' => 'By creating an account, you agree to our',
'consent_and' => 'and',
'is_trainer' => 'I am a trainer', 'is_trainer' => 'I am a trainer',
'equipment_options' => [ 'equipment_options' => [
'dumbbells' => 'Dumbbells', 'dumbbells' => 'Dumbbells',
+8 -6
View File
@@ -127,12 +127,6 @@ return [
'q6' => 'Ce limbi sunt disponibile?', 'q6' => 'Ce limbi sunt disponibile?',
'a6' => 'Engleză și Română. Schimbă limba din antet.', 'a6' => 'Engleză și Română. Schimbă limba din antet.',
], ],
'footer' => [
'about' => 'Despre',
'privacy' => 'Confidențialitate',
'terms' => 'Termeni',
'contact' => 'Contact',
],
], ],
'workouts' => [ 'workouts' => [
@@ -254,6 +248,12 @@ return [
'manage_workouts' => 'Gestionează antrenamentele', 'manage_workouts' => 'Gestionează antrenamentele',
], ],
'footer' => [
'privacy' => 'Politica de confidențialitate',
'terms' => 'Termeni și condiții',
'rights' => 'Toate drepturile rezervate.',
],
'yoga' => [ 'yoga' => [
'browse' => 'Răsfoiește pozițiile de yoga', 'browse' => 'Răsfoiește pozițiile de yoga',
'browse_desc' => 'Explorează pozițiile de yoga după categorie sau beneficii.', 'browse_desc' => 'Explorează pozițiile de yoga după categorie sau beneficii.',
@@ -315,6 +315,8 @@ return [
'back' => 'Înapoi', 'back' => 'Înapoi',
'register' => 'Înregistrare', 'register' => 'Înregistrare',
], ],
'consent_prefix' => 'Prin crearea unui cont, ești de acord cu',
'consent_and' => 'și cu',
'is_trainer' => 'Sunt antrenor', 'is_trainer' => 'Sunt antrenor',
'equipment_options' => [ 'equipment_options' => [
'dumbbells' => 'Gantere', 'dumbbells' => 'Gantere',
+8 -1
View File
@@ -153,7 +153,14 @@
</div> </div>
</div> </div>
<div class="flex items-center justify-between mt-6"> <p class="mt-6 text-xs text-gray-500 dark:text-gray-400">
{{ __('app.auth.register.consent_prefix') }}
<a href="{{ route('legal.terms') }}" target="_blank" class="underline hover:text-gray-700 dark:hover:text-gray-200">{{ __('app.footer.terms') }}</a>
{{ __('app.auth.register.consent_and') }}
<a href="{{ route('legal.privacy') }}" target="_blank" class="underline hover:text-gray-700 dark:hover:text-gray-200">{{ __('app.footer.privacy') }}</a>.
</p>
<div class="flex items-center justify-between mt-4">
<button type="button" @click="goBack()" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800">{{ __('app.auth.register.steps.back') }}</button> <button type="button" @click="goBack()" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800">{{ __('app.auth.register.steps.back') }}</button>
<x-primary-button class="ms-4"> <x-primary-button class="ms-4">
{{ __('app.auth.register.steps.register') }} {{ __('app.auth.register.steps.register') }}
-25
View File
@@ -192,29 +192,4 @@
</div> </div>
</section> </section>
<!-- Footer -->
<footer class="border-t border-gray-200 dark:border-gray-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="text-sm text-gray-600 dark:text-gray-300">© {{ date('Y') }} FitAtlas</div>
<div class="flex items-center gap-6 text-sm">
<a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.home.footer.about') }}</a>
<a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.home.footer.privacy') }}</a>
<a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.home.footer.terms') }}</a>
<a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">{{ __('app.home.footer.contact') }}</a>
</div>
<div class="flex items-center gap-4">
<a href="#" aria-label="Twitter" class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5"><path d="M22 5.92c-.77.34-1.6.57-2.46.67a4.27 4.27 0 0 0 1.87-2.36 8.49 8.49 0 0 1-2.7 1.03 4.24 4.24 0 0 0-7.23 3.87A12.04 12.04 0 0 1 3.15 4.6a4.24 4.24 0 0 0 1.31 5.66 4.2 4.2 0 0 1-1.92-.53v.05c0 2.05 1.46 3.76 3.4 4.15-.36.1-.74.15-1.13.15-.28 0-.55-.03-.81-.08.55 1.73 2.15 2.99 4.05 3.02A8.5 8.5 0 0 1 2 19.54a12 12 0 0 0 6.5 1.9c7.81 0 12.08-6.47 12.08-12.08 0-.18 0-.36-.01-.54A8.64 8.64 0 0 0 22 5.92z"/></svg>
</a>
<a href="#" aria-label="Instagram" class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5"><path d="M7 2C4.243 2 2 4.243 2 7v10c0 2.757 2.243 5 5 5h10c2.757 0 5-2.243 5-5V7c0-2.757-2.243-5-5-5H7zm10 2a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h10zm-5 3a5 5 0 1 0 0 10 5 5 0 0 0 0-10zm0 2.2a2.8 2.8 0 1 1 0 5.6 2.8 2.8 0 0 1 0-5.6zM17.5 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></svg>
</a>
<a href="#" aria-label="YouTube" class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5"><path d="M23.5 6.2a3 3 0 0 0-2.1-2.1C19.6 3.5 12 3.5 12 3.5s-7.6 0-9.4.6A3 3 0 0 0 .5 6.2 31.6 31.6 0 0 0 0 12a31.6 31.6 0 0 0 .5 5.8 3 3 0 0 0 2.1 2.1c1.8.6 9.4.6 9.4.6s7.6 0 9.4-.6a3 3 0 0 0 2.1-2.1 31.6 31.6 0 0 0 .5-5.8 31.6 31.6 0 0 0-.5-5.8zM9.8 15.5v-7l6 3.5-6 3.5z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</x-app-layout> </x-app-layout>
+10
View File
@@ -46,6 +46,16 @@
<main> <main>
{{ $slot }} {{ $slot }}
</main> </main>
<footer class="border-t border-gray-100 dark:border-gray-800 mt-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-gray-500 dark:text-gray-400">
<div>&copy; {{ date('Y') }} {{ config('app.name') }}. {{ __('app.footer.rights') }}</div>
<div class="flex items-center gap-4">
<a href="{{ route('legal.privacy') }}" class="hover:text-gray-800 dark:hover:text-gray-200">{{ __('app.footer.privacy') }}</a>
<a href="{{ route('legal.terms') }}" class="hover:text-gray-800 dark:hover:text-gray-200">{{ __('app.footer.terms') }}</a>
</div>
</div>
</footer>
</div> </div>
@stack('scripts') @stack('scripts')
</body> </body>
+98
View File
@@ -0,0 +1,98 @@
<x-app-layout>
<section class="bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800">
<div class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">Privacy Policy</h1>
<p class="mt-2 text-sm text-gray-500">Last updated: August 10, 2026</p>
</div>
</section>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8 space-y-8 text-gray-800 dark:text-gray-200 leading-relaxed">
<p class="rounded-xl bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-200 px-4 py-3 text-sm">
This policy is a starting point, not a substitute for legal advice. Before launch, replace the bracketed
placeholders below with your actual operator details and have it reviewed for the jurisdictions you operate in.
</p>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Who this policy covers</h2>
<p class="mt-2">
This Privacy Policy explains how {{ config('app.name') }} ("we", "us") collects, uses, and stores
information when you use this application. {{ config('app.name') }} is operated by
[Operator Name / Legal Entity]. If you have questions about this policy, contact us at
<a href="mailto:privacy@example.com" class="text-indigo-600 hover:text-indigo-500">privacy@example.com</a>.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Information we collect</h2>
<p class="mt-2">When you create an account, we collect:</p>
<ul class="mt-2 list-disc ms-6 space-y-1">
<li><strong>Account details:</strong> name, email address, and password (stored as a salted hash, never in plain text).</li>
<li>
<strong>Fitness profile (optional):</strong> age, weight, height, fitness level, goals, health
notes, available equipment, and training preferences. You choose whether to provide these when
you sign up or from your profile settings.
</li>
<li><strong>Activity data:</strong> the workouts you build, the exercises and yoga poses you favorite, and your workout session history (what you completed and when).</li>
</ul>
<p class="mt-3">
Health-related fields such as health notes, weight, and fitness goals can be considered sensitive
personal data in some jurisdictions. We only collect them if you choose to enter them, and only to
power features like workout recommendations and progress tracking.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Cookies</h2>
<p class="mt-2">
We use strictly necessary cookies to keep you signed in and to protect the site against
cross-site request forgery. We do not use advertising or third-party tracking cookies.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">How we use your information</h2>
<ul class="mt-2 list-disc ms-6 space-y-1">
<li>To provide the core features of the app: workout building, session tracking, and your dashboard.</li>
<li>To personalize the exercise and yoga library based on your stated goals and equipment.</li>
<li>To send account-related emails, such as email verification and password resets.</li>
</ul>
<p class="mt-3">We do not sell your personal information, and we do not use your data for advertising.</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Third parties</h2>
<p class="mt-2">
Our exercise and yoga pose libraries are sourced from third-party public data providers. Browsing
that content does not send your personal data to those providers. We do not share your account or
fitness data with any third party except as required to operate the service (for example, hosting
infrastructure) or as required by law.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Data retention and deletion</h2>
<p class="mt-2">
You can permanently delete your account and all associated data (workouts, sessions, and favorites)
at any time from your <a href="{{ route('profile.edit') }}" class="text-indigo-600 hover:text-indigo-500">profile settings</a>.
Deletion is immediate and cannot be undone.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Your rights</h2>
<p class="mt-2">
Depending on where you live, you may have the right to access, correct, export, or delete your
personal data. You can update most of your information directly from your profile, or contact us
at <a href="mailto:privacy@example.com" class="text-indigo-600 hover:text-indigo-500">privacy@example.com</a> for anything else.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Changes to this policy</h2>
<p class="mt-2">
If we make material changes to this policy, we will update the "Last updated" date above and, where
appropriate, notify you directly.
</p>
</div>
</section>
</x-app-layout>
+102
View File
@@ -0,0 +1,102 @@
<x-app-layout>
<section class="bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800">
<div class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">Terms of Service</h1>
<p class="mt-2 text-sm text-gray-500">Last updated: August 10, 2026</p>
</div>
</section>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8 space-y-8 text-gray-800 dark:text-gray-200 leading-relaxed">
<p class="rounded-xl bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-200 px-4 py-3 text-sm">
This is a starting point, not a substitute for legal advice. Before launch, replace the bracketed
placeholders below with your actual operator details and have it reviewed for the jurisdictions you operate in.
</p>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">1. Agreement</h2>
<p class="mt-2">
These Terms of Service govern your use of {{ config('app.name') }}, operated by
[Operator Name / Legal Entity]. By creating an account or using the app, you agree to these terms.
If you do not agree, please do not use the app.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">2. Not medical advice</h2>
<p class="mt-2">
{{ config('app.name') }} provides general fitness and exercise content for informational purposes
only. It is not a substitute for professional medical advice, diagnosis, or treatment. Consult a
physician before starting any exercise program, especially if you have an existing health condition,
injury, or are pregnant. You are responsible for exercising within your own physical limits.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">3. Your account</h2>
<ul class="mt-2 list-disc ms-6 space-y-1">
<li>You must provide accurate information when creating an account.</li>
<li>You are responsible for keeping your password secure and for all activity under your account.</li>
<li>You must be legally permitted to enter into these terms in your jurisdiction to register.</li>
</ul>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">4. Trainer accounts</h2>
<p class="mt-2">
If you register as a trainer, you remain solely responsible for the accuracy and safety of any
guidance you provide to others through or alongside the app. We do not verify trainer credentials.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">5. Acceptable use</h2>
<p class="mt-2">You agree not to:</p>
<ul class="mt-2 list-disc ms-6 space-y-1">
<li>Use the app for any unlawful purpose or in violation of these terms.</li>
<li>Attempt to gain unauthorized access to other accounts or to the admin panel.</li>
<li>Interfere with or disrupt the app's infrastructure or scrape it at abusive volume.</li>
<li>Upload content to the blog or your profile that is unlawful, infringing, or harmful.</li>
</ul>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">6. Content</h2>
<p class="mt-2">
Exercise and yoga pose data is sourced from third-party providers and is provided "as is." Blog
content reflects the views of its authors and is provided for general informational purposes.
Workouts, notes, and other content you create remain yours; by using the app you allow us to store
and display it back to you as part of the service.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">7. Termination</h2>
<p class="mt-2">
You may delete your account at any time from your profile settings. We may suspend or terminate
accounts that violate these terms.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">8. Disclaimer and limitation of liability</h2>
<p class="mt-2">
The app is provided "as is" without warranties of any kind. To the fullest extent permitted by law,
{{ config('app.name') }} and its operator are not liable for any injury, loss, or damage arising
from your use of the app, including from following exercise or yoga content it provides.
</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">9. Governing law</h2>
<p class="mt-2">These terms are governed by the laws of [Your Jurisdiction], without regard to conflict-of-law principles.</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">10. Contact</h2>
<p class="mt-2">
Questions about these terms can be sent to
<a href="mailto:legal@example.com" class="text-indigo-600 hover:text-indigo-500">legal@example.com</a>.
</p>
</div>
</section>
</x-app-layout>
+4
View File
@@ -71,4 +71,8 @@ Route::prefix('/yoga')->group(function () {
Route::get('/{pose:slug}', [\App\Http\Controllers\YogaPoseController::class, 'show'])->name('yoga.show'); Route::get('/{pose:slug}', [\App\Http\Controllers\YogaPoseController::class, 'show'])->name('yoga.show');
}); });
// Legal
Route::get('/privacy', [\App\Http\Controllers\LegalController::class, 'privacy'])->name('legal.privacy');
Route::get('/terms', [\App\Http\Controllers\LegalController::class, 'terms'])->name('legal.terms');
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LegalTest extends TestCase
{
use RefreshDatabase;
public function test_privacy_policy_page_loads(): void
{
$response = $this->get(route('legal.privacy'));
$response->assertOk();
$response->assertSee('Privacy Policy');
}
public function test_terms_page_loads(): void
{
$response = $this->get(route('legal.terms'));
$response->assertOk();
$response->assertSee('Terms of Service');
}
public function test_register_page_links_to_legal_pages(): void
{
$response = $this->get(route('register'));
$response->assertOk();
$response->assertSee(route('legal.privacy'), false);
$response->assertSee(route('legal.terms'), false);
}
}