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