feat: add terms, privacy pages, email notification preferences, and sign-in consent (#107)

- /terms — Terms of Service for editor + pascal.app platform
- /privacy — Privacy Policy (data collection, third parties, user rights)
- Email notification toggle in Settings (new column + migration)
- Sign-in consent text linking to terms/privacy
- Effective date: February 20, 2026

Co-authored-by: Anton Pascal <anton-pascal@users.noreply.github.com>
This commit is contained in:
Anton
2026-02-20 04:02:08 +00:00
committed by GitHub
co-authored by Anton Pascal
parent f4f9f8a220
commit 2dcb80554c
8 changed files with 464 additions and 2 deletions
+207
View File
@@ -0,0 +1,207 @@
import type { Metadata } from 'next'
import Link from 'next/link'
export const metadata: Metadata = {
title: 'Privacy Policy',
description: 'Privacy Policy for Pascal Editor and the Pascal platform.',
}
export default function PrivacyPage() {
return (
<div className="min-h-screen bg-background">
<header className="border-b border-border bg-background/95 backdrop-blur sticky top-0 z-10">
<div className="container mx-auto px-6 py-4">
<nav className="flex items-center gap-4 text-sm">
<Link
href="/"
className="text-muted-foreground transition-colors hover:text-foreground"
>
Home
</Link>
<span className="text-muted-foreground">/</span>
<Link
href="/terms"
className="text-muted-foreground transition-colors hover:text-foreground"
>
Terms of Service
</Link>
<span className="text-muted-foreground">|</span>
<span className="text-foreground font-medium">Privacy Policy</span>
</nav>
</div>
</header>
<main className="container mx-auto max-w-3xl px-6 py-12">
<article className="prose prose-neutral dark:prose-invert max-w-none">
<h1 className="text-3xl font-bold mb-2">Privacy Policy</h1>
<p className="text-muted-foreground text-sm mb-8">
Effective Date: February 20, 2026
</p>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">1. Introduction</h2>
<p className="text-foreground/90 leading-relaxed">
Pascal Group Inc. (&quot;we,&quot; &quot;us,&quot; or &quot;our&quot;) operates the Pascal Editor and
Platform at pascal.app. This Privacy Policy explains how we collect, use, and
protect your information when you use our services.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">2. Information We Collect</h2>
<h3 className="text-lg font-medium mt-4">Account Information</h3>
<p className="text-foreground/90 leading-relaxed">
When you create an account, we collect:
</p>
<ul className="list-disc pl-6 space-y-2 text-foreground/90">
<li>Email address</li>
<li>Name</li>
<li>Profile picture/avatar</li>
<li>OAuth provider data (from Google when you sign in with Google)</li>
</ul>
<h3 className="text-lg font-medium mt-4">Project Data</h3>
<p className="text-foreground/90 leading-relaxed">
When you use the Platform, we store your projects, including 3D building designs,
floor plans, and associated metadata.
</p>
<h3 className="text-lg font-medium mt-4">Usage Analytics</h3>
<p className="text-foreground/90 leading-relaxed">
We use Vercel Analytics and Speed Insights to collect anonymized usage data,
including page views, performance metrics, and general usage patterns. This helps
us improve the Platform.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">3. How We Use Your Information</h2>
<p className="text-foreground/90 leading-relaxed">We use your information to:</p>
<ul className="list-disc pl-6 space-y-2 text-foreground/90">
<li>Provide and maintain your account</li>
<li>Store and sync your projects across devices</li>
<li>Improve our services based on usage patterns</li>
<li>Send optional email notifications about new features and updates (you can opt out in settings)</li>
<li>Respond to support requests</li>
<li>Ensure platform security and prevent abuse</li>
</ul>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">4. Data Storage</h2>
<p className="text-foreground/90 leading-relaxed">
Your data is stored using Supabase (PostgreSQL database) on secure cloud
infrastructure. We implement appropriate technical and organizational measures
to protect your data.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">5. Third-Party Services</h2>
<p className="text-foreground/90 leading-relaxed">
We use the following third-party services to operate the Platform:
</p>
<ul className="list-disc pl-6 space-y-2 text-foreground/90">
<li>
<strong>Google</strong> - OAuth authentication for sign-in
</li>
<li>
<strong>Vercel</strong> - Application hosting, analytics, and performance monitoring
</li>
<li>
<strong>Supabase</strong> - Database hosting and authentication infrastructure
</li>
</ul>
<p className="text-foreground/90 leading-relaxed mt-4">
Each of these services has their own privacy policies governing their handling
of your data.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">6. Cookies</h2>
<p className="text-foreground/90 leading-relaxed">
We use minimal cookies necessary for the Platform to function:
</p>
<ul className="list-disc pl-6 space-y-2 text-foreground/90">
<li>
<strong>Session cookies</strong> - Essential for authentication and keeping you
signed in
</li>
<li>
<strong>Analytics cookies</strong> - Used by Vercel Analytics to collect
anonymized usage data
</li>
</ul>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">7. Your Rights</h2>
<p className="text-foreground/90 leading-relaxed">You have the right to:</p>
<ul className="list-disc pl-6 space-y-2 text-foreground/90">
<li>Access the personal data we hold about you</li>
<li>Request correction of inaccurate data</li>
<li>Request deletion of your data</li>
<li>Export your project data</li>
<li>Opt out of marketing communications</li>
</ul>
<p className="text-foreground/90 leading-relaxed mt-4">
To exercise any of these rights, please contact us at{' '}
<a
href="mailto:support@pascal.app"
className="text-foreground underline hover:text-foreground/80"
>
support@pascal.app
</a>
.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">8. Data Retention</h2>
<p className="text-foreground/90 leading-relaxed">
We retain your data for as long as your account is active. If you delete your
account, we will delete your personal data and project data within 30 days,
except where we are required by law to retain certain information.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">9. Children&apos;s Privacy</h2>
<p className="text-foreground/90 leading-relaxed">
The Platform is not intended for children under 13. We do not knowingly collect
personal information from children under 13. If you believe we have collected
such information, please contact us immediately.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">10. Changes to This Policy</h2>
<p className="text-foreground/90 leading-relaxed">
We may update this Privacy Policy from time to time. We will notify you of
material changes by posting the updated policy on the Platform. Your continued
use of the Platform after changes are posted constitutes your acceptance of the
revised policy.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-semibold">11. Contact Us</h2>
<p className="text-foreground/90 leading-relaxed">
If you have questions about this Privacy Policy or how we handle your data,
please contact us at{' '}
<a
href="mailto:support@pascal.app"
className="text-foreground underline hover:text-foreground/80"
>
support@pascal.app
</a>
.
</p>
</section>
</article>
</main>
</div>
)
}
+1
View File
@@ -32,6 +32,7 @@ export default async function Settings() {
currentUsername={profile?.username ?? null}
currentGithubUrl={profile?.githubUrl ?? null}
currentXUrl={profile?.xUrl ?? null}
currentEmailNotifications={profile?.emailNotifications ?? true}
connectedAccounts={connectedAccounts}
/>
)
+177
View File
@@ -0,0 +1,177 @@
import type { Metadata } from 'next'
import Link from 'next/link'
export const metadata: Metadata = {
title: 'Terms of Service',
description: 'Terms of Service for Pascal Editor and the Pascal platform.',
}
export default function TermsPage() {
return (
<div className="min-h-screen bg-background">
<header className="border-b border-border bg-background/95 backdrop-blur sticky top-0 z-10">
<div className="container mx-auto px-6 py-4">
<nav className="flex items-center gap-4 text-sm">
<Link
href="/"
className="text-muted-foreground transition-colors hover:text-foreground"
>
Home
</Link>
<span className="text-muted-foreground">/</span>
<span className="text-foreground font-medium">Terms of Service</span>
<span className="text-muted-foreground">|</span>
<Link
href="/privacy"
className="text-muted-foreground transition-colors hover:text-foreground"
>
Privacy Policy
</Link>
</nav>
</div>
</header>
<main className="container mx-auto max-w-3xl px-6 py-12">
<article className="prose prose-neutral dark:prose-invert max-w-none">
<h1 className="text-3xl font-bold mb-2">Terms of Service</h1>
<p className="text-muted-foreground text-sm mb-8">
Effective Date: February 20, 2026
</p>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">1. Introduction</h2>
<p className="text-foreground/90 leading-relaxed">
Welcome to Pascal Editor (&quot;Editor&quot;) and the Pascal platform at pascal.app
(&quot;Platform&quot;), operated by Pascal Group Inc. (&quot;we,&quot; &quot;us,&quot; or &quot;our&quot;).
By accessing or using our services, you agree to these Terms of Service.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">2. The Editor and Platform</h2>
<p className="text-foreground/90 leading-relaxed">
The Pascal Editor is open-source software released under the MIT License.
You may use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Editor software in accordance with the MIT License terms.
</p>
<p className="text-foreground/90 leading-relaxed">
The Pascal platform (pascal.app) and its associated services, including user accounts,
cloud storage, and project hosting, are proprietary services owned and operated by
Pascal Group Inc. These Terms govern your use of the Platform.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">3. Accounts and Authentication</h2>
<p className="text-foreground/90 leading-relaxed">
To use certain features of the Platform, you must create an account. We use
Google OAuth and magic link email authentication through Supabase. You are
responsible for maintaining the security of your account credentials and for
all activities that occur under your account.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">4. Acceptable Use</h2>
<p className="text-foreground/90 leading-relaxed">You agree not to:</p>
<ul className="list-disc pl-6 space-y-2 text-foreground/90">
<li>Use the Platform for any unlawful purpose or in violation of any applicable laws</li>
<li>Upload, share, or distribute content that infringes intellectual property rights</li>
<li>Attempt to gain unauthorized access to the Platform or its systems</li>
<li>Interfere with or disrupt the Platform&apos;s infrastructure</li>
<li>Upload malicious code, viruses, or harmful content</li>
<li>Harass, abuse, or harm other users</li>
<li>Use the Platform to send spam or unsolicited communications</li>
</ul>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">5. Your Content and Intellectual Property</h2>
<p className="text-foreground/90 leading-relaxed">
You retain full ownership of all content, projects, and data you create or upload
to the Platform (&quot;Your Content&quot;). By using the Platform, you grant us a limited
license to store, display, and transmit Your Content solely to provide our services
to you.
</p>
<p className="text-foreground/90 leading-relaxed">
We do not claim any ownership rights over Your Content. You may export or delete
Your Content at any time.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">6. Platform Ownership</h2>
<p className="text-foreground/90 leading-relaxed">
The Platform, including its design, features, and proprietary code, is owned by
Pascal Group Inc. and protected by intellectual property laws. While the Editor
source code is open-source under the MIT License, the Platform services, branding,
and infrastructure remain our proprietary property.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">7. Account Termination</h2>
<p className="text-foreground/90 leading-relaxed">
We reserve the right to suspend or terminate your account if you violate these
Terms or engage in conduct that we determine is harmful to the Platform or other
users. You may also delete your account at any time by contacting us at{' '}
<a
href="mailto:support@pascal.app"
className="text-foreground underline hover:text-foreground/80"
>
support@pascal.app
</a>
.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">8. Disclaimer of Warranties</h2>
<p className="text-foreground/90 leading-relaxed">
THE PLATFORM IS PROVIDED &quot;AS IS&quot; AND &quot;AS AVAILABLE&quot; WITHOUT WARRANTIES OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
</p>
<p className="text-foreground/90 leading-relaxed">
We do not warrant that the Platform will be uninterrupted, error-free, or free
of harmful components.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">9. Limitation of Liability</h2>
<p className="text-foreground/90 leading-relaxed">
TO THE MAXIMUM EXTENT PERMITTED BY LAW, PASCAL GROUP INC. SHALL NOT BE LIABLE
FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES,
INCLUDING LOSS OF DATA, PROFITS, OR GOODWILL, ARISING FROM YOUR USE OF THE
PLATFORM.
</p>
</section>
<section className="space-y-4 mb-8">
<h2 className="text-xl font-semibold">10. Changes to Terms</h2>
<p className="text-foreground/90 leading-relaxed">
We may update these Terms from time to time. We will notify you of material
changes by posting the updated Terms on the Platform. Your continued use of the
Platform after changes are posted constitutes your acceptance of the revised Terms.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-semibold">11. Contact Us</h2>
<p className="text-foreground/90 leading-relaxed">
If you have questions about these Terms, please contact us at{' '}
<a
href="mailto:support@pascal.app"
className="text-foreground underline hover:text-foreground/80"
>
support@pascal.app
</a>
.
</p>
</section>
</article>
</main>
</div>
)
}
@@ -5,7 +5,7 @@ import Link from 'next/link'
import { ArrowLeft, Pencil } from 'lucide-react'
import { useRef, useState } from 'react'
import { authClient } from '../lib/auth/client'
import { updateUsername, updateProfile, uploadAvatar } from '../lib/auth/actions'
import { updateUsername, updateProfile, uploadAvatar, updateEmailNotifications } from '../lib/auth/actions'
function GoogleIcon({ className }: { className?: string }) {
return (
@@ -40,6 +40,7 @@ interface SettingsPageProps {
currentUsername: string | null
currentGithubUrl: string | null
currentXUrl: string | null
currentEmailNotifications: boolean
connectedAccounts: { providerId: string; accountId: string }[]
}
@@ -48,6 +49,7 @@ export function SettingsPage({
currentUsername,
currentGithubUrl,
currentXUrl,
currentEmailNotifications,
connectedAccounts,
}: SettingsPageProps) {
const [username, setUsername] = useState(currentUsername ?? '')
@@ -67,6 +69,8 @@ export function SettingsPage({
type: 'success' | 'error'
text: string
} | null>(null)
const [emailNotifications, setEmailNotifications] = useState(currentEmailNotifications)
const [isSavingNotifications, setIsSavingNotifications] = useState(false)
const isGoogleConnected = connectedAccounts.some((a) => a.providerId === 'google')
const initials = currentUsername
@@ -149,6 +153,14 @@ export function SettingsPage({
(githubUrl.trim() || '') !== (currentGithubUrl ?? '') ||
(xUrl.trim() || '') !== (currentXUrl ?? '')
const handleToggleEmailNotifications = async () => {
const newValue = !emailNotifications
setEmailNotifications(newValue)
setIsSavingNotifications(true)
await updateEmailNotifications(newValue)
setIsSavingNotifications(false)
}
return (
<div className="min-h-screen bg-background">
<header className="border-b border-border bg-background/95 backdrop-blur sticky top-0 z-10">
@@ -305,6 +317,37 @@ export function SettingsPage({
</div>
</section>
{/* Notifications Section */}
<section className="space-y-4">
<h2 className="text-lg font-semibold">Notifications</h2>
<div className="rounded-lg border border-border p-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="text-sm font-medium">Email notifications</div>
<p className="text-muted-foreground text-xs">
Receive emails about new features and updates.
</p>
</div>
<button
type="button"
role="switch"
aria-checked={emailNotifications}
onClick={handleToggleEmailNotifications}
disabled={isSavingNotifications}
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${
emailNotifications ? 'bg-primary' : 'bg-input'
}`}
>
<span
className={`pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform ${
emailNotifications ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
</div>
</div>
</section>
{/* Social Links Section */}
<section className="space-y-4">
<h2 className="text-lg font-semibold">Social Links</h2>
@@ -1,5 +1,6 @@
'use client'
import Link from 'next/link'
import { Mail, X } from 'lucide-react'
import { useState } from 'react'
import { authClient } from '../lib/auth/client'
@@ -218,7 +219,15 @@ export function SignInDialog({ open, onOpenChange }: SignInDialogProps) {
</form>
<p className="text-center text-muted-foreground text-xs">
Sign in with Google or receive a magic link via email.
By signing in, you agree to our{' '}
<Link href="/terms" className="underline hover:text-foreground">
Terms of Service
</Link>{' '}
and{' '}
<Link href="/privacy" className="underline hover:text-foreground">
Privacy Policy
</Link>
.
</p>
</div>
)}
@@ -122,6 +122,7 @@ export async function getUserProfile(): Promise<{
username: string | null
githubUrl: string | null
xUrl: string | null
emailNotifications: boolean
} | null> {
const session = await getSession()
if (!session?.user) return null
@@ -131,6 +132,7 @@ export async function getUserProfile(): Promise<{
username: schema.users.username,
githubUrl: schema.users.githubUrl,
xUrl: schema.users.xUrl,
emailNotifications: schema.users.emailNotifications,
})
.from(schema.users)
.where(eq(schema.users.id, session.user.id))
@@ -278,3 +280,23 @@ export async function uploadAvatar(
revalidatePath('/settings')
return { success: true, imageUrl }
}
/**
* Update the current user's email notification preference
*/
export async function updateEmailNotifications(
enabled: boolean,
): Promise<{ success: boolean; error?: string }> {
const session = await getSession()
if (!session?.user) {
return { success: false, error: 'Not authenticated' }
}
await db
.update(schema.users)
.set({ emailNotifications: enabled })
.where(eq(schema.users.id, session.user.id))
revalidatePath('/settings')
return { success: true }
}