This commit is contained in:
Jamie Brunton committed 2025-12-30 20:48:14 +00:00
1 parent edcc5a6670
commit 1b3f1d9409
45 files changed
+14500 -185

No files matched your search

@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea';
import {
Dialog,
DialogContent,
@@ -44,6 +45,8 @@ export function AiGeneratorDialog({
// Default tone based on pack's explicit flag (5 for explicit packs, 3 for non-explicit)
const [tone, setTone] = useState<ToneLevel>(context?.isExplicit ? 5 : 3);
const isExplicit = tone >= 4; // For other generators that still use boolean
const [crudeLang, setCrudeLang] = useState(false);
const [inspiration, setInspiration] = useState('');
// Update tone when dialog opens with new context
useEffect(() => {
@@ -65,26 +68,26 @@ export function AiGeneratorDialog({
setLoading(true);
try {
if (type === 'pack') {
const result = await generatePack(context?.categoryName, isExplicit);
const result = await generatePack(context?.categoryName, isExplicit, crudeLang, inspiration || undefined);
setGeneratedPack(result);
} else if (type === 'questions') {
if (!context?.packName) {
toast.error('Pack name is required');
return;
}
const result = await generateQuestions(context.packName, count, undefined, tone, context.packDescription || undefined, context.existingQuestions);
const result = await generateQuestions(context.packName, count, undefined, tone, context.packDescription || undefined, context.existingQuestions, crudeLang, inspiration || undefined);
setGeneratedQuestions(result);
// Select all questions by default
setSelectedQuestionIndices(new Set(result.map((_, i) => i)));
} else if (type === 'category-ideas') {
const result = await suggestCategories(context?.existingCategories || [], isExplicit);
const result = await suggestCategories(context?.existingCategories || [], isExplicit, crudeLang, inspiration || undefined);
setGeneratedIdeas(result);
} else if (type === 'category-pack-ideas') {
if (!context?.categoryName) {
toast.error('Category name is required');
return;
}
const result = await suggestPacks(context.categoryName, context?.existingPacks || [], isExplicit);
const result = await suggestPacks(context.categoryName, context?.existingPacks || [], isExplicit, crudeLang, inspiration || undefined);
setGeneratedPackIdeas(result);
}
} catch (error) {
@@ -136,6 +139,8 @@ export function AiGeneratorDialog({
setGeneratedIdeas([]);
setGeneratedPackIdeas([]);
setSelectedQuestionIndices(new Set());
setCrudeLang(false);
setInspiration('');
onOpenChange(false);
};
@@ -212,6 +217,33 @@ export function AiGeneratorDialog({
</div>
)}
{/* Crude Language Toggle */}
<div className="flex items-center space-x-2">
<Switch
id="crude-lang"
checked={crudeLang}
onCheckedChange={setCrudeLang}
/>
<Label htmlFor="crude-lang">Crude Language</Label>
<span className="text-xs text-muted-foreground">(vulgar, raw, everyday explicit words)</span>
</div>
{/* Inspiration/Suggestions Textarea */}
<div className="space-y-2">
<Label htmlFor="inspiration">Inspiration / Suggestions (optional)</Label>
<Textarea
id="inspiration"
placeholder="Provide any themes, ideas, or guidance for the AI to consider..."
value={inspiration}
onChange={(e) => setInspiration(e.target.value)}
rows={3}
className="resize-none"
/>
<p className="text-xs text-muted-foreground">
Add freetext suggestions to guide the AI generation
</p>
</div>
{/* Context info */}
{context?.categoryName && (
<div className="rounded-md bg-muted p-3 text-sm">
+52 -7
View File
@@ -58,13 +58,26 @@ export interface GeneratedQuestion {
intensity: number;
}
export async function generatePack(categoryName?: string, explicit: boolean = false): Promise<GeneratedPack> {
export async function generatePack(
categoryName?: string,
explicit: boolean = false,
crudeLang: boolean = false,
inspiration?: string
): Promise<GeneratedPack> {
const openai = getOpenAI();
const explicitInstruction = explicit
? 'Include bold, spicy, and uncensored themes (NSFW is allowed).'
: 'Do NOT include any explicit or NSFW themes. Keep it romantic, emotional, playful, and clean.';
const crudeLangInstruction = crudeLang
? 'Use crude, vulgar, and raw language. Be blunt and use everyday explicit words.'
: 'Use tasteful, refined language. Avoid crude or vulgar terms even for explicit content.';
const inspirationInstruction = inspiration
? `\n\nINSPIRATION/GUIDANCE FROM ADMIN:\n${inspiration}\n\nUse the above inspiration to guide your generation.`
: '';
const appContext = `This is an ACTIVITY-FOCUSED app where couples swipe on activity proposals.
Each partner independently swipes Like/Dislike/Maybe, and when both swipe positively they "match".
Packs contain collections of activities to try together (date ideas, intimate experiences, adventures, challenges, etc.).
@@ -78,6 +91,7 @@ This is NOT a Q&A app - it's about discovering shared interests in activities.`;
The pack should contain a themed collection of activities couples can explore together.
${explicitInstruction}
${crudeLangInstruction}${inspirationInstruction}
Return a JSON object with:
- name: A catchy, engaging pack name (3-6 words) that evokes activities/experiences
@@ -91,6 +105,7 @@ This is NOT a Q&A app - it's about discovering shared interests in activities.`;
The pack should contain a themed collection of activities couples can explore together.
${explicitInstruction}
${crudeLangInstruction}${inspirationInstruction}
Return a JSON object with:
- name: A catchy, engaging pack name (3-6 words) that evokes activities/experiences
@@ -198,12 +213,22 @@ export async function generateQuestions(
intensity?: number,
tone: ToneLevel = 3,
packDescription?: string,
existingQuestions?: string[]
existingQuestions?: string[],
crudeLang: boolean = false,
inspiration?: string
): Promise<GeneratedQuestion[]> {
const openai = getOpenAI();
const isClean = tone === 0;
const isExplicit = tone >= 4;
const crudeLangInstruction = crudeLang
? '\n\nCRUDE LANGUAGE: Use crude, vulgar, raw language throughout. Be blunt, direct, and use everyday explicit words like people actually use in real life. Avoid euphemisms or clinical terms.'
: '\n\nLANGUAGE STYLE: Use tasteful, refined language. Avoid crude or vulgar terms. For explicit content, use direct but non-crude terms (e.g., "oral sex" not "blowjob", "have sex" not "fuck").';
const inspirationInstruction = inspiration
? `\n\nINSPIRATION/GUIDANCE FROM ADMIN:\n${inspiration}\n\nUse the above inspiration to guide the types of questions you generate.`
: '';
const intensityInstruction = isClean
? 'All questions should be intensity level 1 (non-physical activities).'
: intensity
@@ -266,7 +291,7 @@ CRITICAL: Generate completely NEW and DIFFERENT questions. Do not repeat any of
${INTENSITY_GUIDE_SHORT}
${intensityInstruction}
${toneInstruction}${explicitWarning}
${toneInstruction}${explicitWarning}${crudeLangInstruction}${inspirationInstruction}
IMPORTANT: The app uses a swipe-based interface (Like/Dislike/Maybe).
Cards should be "Proposals" for specific actions, NOT interview questions.
@@ -371,7 +396,9 @@ export interface GeneratedCategoryIdea {
export async function suggestCategories(
existingCategories: string[],
explicit: boolean
explicit: boolean,
crudeLang: boolean = false,
inspiration?: string
): Promise<GeneratedCategoryIdea[]> {
const openai = getOpenAI();
const existingList = existingCategories.length > 0
@@ -382,6 +409,14 @@ export async function suggestCategories(
? 'Include bold, spicy, and explicitly intimate categories (NSFW is allowed).'
: 'Do NOT include any explicit or NSFW themes. Keep it romantic, emotional, playful, and clean.';
const crudeLangInstruction = crudeLang
? '\nUse crude, vulgar, and raw language in the names and descriptions.'
: '\nUse tasteful, refined language. Avoid crude or vulgar terms.';
const inspirationInstruction = inspiration
? `\n\nINSPIRATION/GUIDANCE FROM ADMIN:\n${inspiration}\n\nUse the above inspiration to guide your category suggestions.`
: '';
const prompt = `Here are the current categories in our couples' activity/intimacy app: ${existingList}.
IMPORTANT - HOW THE APP WORKS:
@@ -392,7 +427,7 @@ IMPORTANT - HOW THE APP WORKS:
- This is NOT a Q&A app - it's about discovering shared interests in activities to do together
Suggest 5 NEW, UNIQUE category ideas that differ from the existing ones.
${explicitInstruction}
${explicitInstruction}${crudeLangInstruction}${inspirationInstruction}
Return a JSON object with an "ideas" array containing 5 objects, where each object has:
- name: Category name (1-3 words)
@@ -429,7 +464,9 @@ export interface GeneratedPackIdea {
export async function suggestPacks(
categoryName: string,
existingPacks: string[],
explicit: boolean
explicit: boolean,
crudeLang: boolean = false,
inspiration?: string
): Promise<GeneratedPackIdea[]> {
const openai = getOpenAI();
const existingList = existingPacks.length > 0
@@ -440,6 +477,14 @@ export async function suggestPacks(
? 'Include bold, spicy, and uncensored pack ideas (NSFW is allowed).'
: 'Do NOT include any explicit or NSFW themes. Keep it romantic, emotional, playful, and clean.';
const crudeLangInstruction = crudeLang
? '\nUse crude, vulgar, and raw language in the pack names and descriptions.'
: '\nUse tasteful, refined language. Avoid crude or vulgar terms.';
const inspirationInstruction = inspiration
? `\n\nINSPIRATION/GUIDANCE FROM ADMIN:\n${inspiration}\n\nUse the above inspiration to guide your pack suggestions.`
: '';
const prompt = `We are building activity packs for the category "${categoryName}" in a couples' intimacy app.
IMPORTANT - HOW THE APP WORKS:
@@ -459,7 +504,7 @@ PACK THEMES should focus on:
Existing packs in this category: ${existingList}.
Suggest 5 NEW, UNIQUE pack ideas that fit this category and differ from existing ones.
${explicitInstruction}
${explicitInstruction}${crudeLangInstruction}${inspirationInstruction}
Return a JSON object with an "ideas" array containing 5 objects, where each object has:
- name: Pack name (catchy, 3-6 words) - should evoke activities/experiences, not questions
+30
View File
@@ -0,0 +1,30 @@
# Dependencies
node_modules
npm-debug.log
# Build outputs
.next
out
build
# Development
.env*.local
.git
.gitignore
# IDE
.idea
.vscode
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Testing
coverage
# Misc
README.md
docker-compose.yml
+31
View File
@@ -0,0 +1,31 @@
# Dependencies
node_modules
.pnp
.pnp.js
# Testing
coverage
# Next.js
.next/
out/
build
# Misc
.DS_Store
*.pem
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Local env files
.env*.local
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
next-env.d.ts
+40
View File
@@ -0,0 +1,40 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built assets from builder
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
+160
View File
@@ -0,0 +1,160 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
:root {
--background: #0d0d1a;
--background-light: #1a1a2e;
--primary: #e94560;
--secondary: #9b59b6;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
scroll-behavior: smooth;
}
body {
background: var(--background);
color: white;
font-family: 'Inter', system-ui, sans-serif;
}
::selection {
background: rgba(233, 69, 96, 0.3);
color: white;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--background);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Glass card effect */
.glass {
background: rgba(22, 33, 62, 0.4);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
}
.glass-light {
background: rgba(22, 33, 62, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
}
/* Gradient text */
.gradient-text {
background: linear-gradient(135deg, #e94560 0%, #9b59b6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Gradient button */
.btn-primary {
background: linear-gradient(135deg, #e94560 0%, #9b59b6 100%);
color: white;
font-weight: 600;
padding: 14px 32px;
border-radius: 12px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(233, 69, 96, 0.4);
}
.btn-secondary {
background: rgba(22, 33, 62, 0.6);
color: white;
font-weight: 600;
padding: 14px 32px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
cursor: pointer;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: rgba(22, 33, 62, 0.8);
border-color: rgba(255, 255, 255, 0.2);
}
/* Glow effects */
.glow-primary {
box-shadow: 0 0 40px rgba(233, 69, 96, 0.3);
}
.glow-secondary {
box-shadow: 0 0 40px rgba(155, 89, 182, 0.3);
}
/* Animation keyframes */
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
@keyframes pulse-glow {
0%, 100% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.1);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-pulse-glow {
animation: pulse-glow 4s ease-in-out infinite;
}
+39
View File
@@ -0,0 +1,39 @@
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'Sauci - Ignite Your Connection',
description: 'Discover what you both desire. Sauci is the couples intimacy app that helps partners explore their connection through fun, meaningful questions.',
keywords: ['couples app', 'intimacy', 'relationship', 'connection', 'dating', 'love'],
authors: [{ name: 'Sauci' }],
openGraph: {
title: 'Sauci - Ignite Your Connection',
description: 'Discover what you both desire. The couples intimacy app for deeper connections.',
url: 'https://sauci.app',
siteName: 'Sauci',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Sauci - Ignite Your Connection',
description: 'Discover what you both desire. The couples intimacy app for deeper connections.',
},
icons: {
icon: '/favicon.png',
apple: '/apple-touch-icon.png',
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className="antialiased">
{children}
</body>
</html>
)
}
+19
View File
@@ -0,0 +1,19 @@
import Navbar from '@/components/Navbar'
import Hero from '@/components/Hero'
import Features from '@/components/Features'
import HowItWorks from '@/components/HowItWorks'
import CTA from '@/components/CTA'
import Footer from '@/components/Footer'
export default function Home() {
return (
<main className="min-h-screen">
<Navbar />
<Hero />
<Features />
<HowItWorks />
<CTA />
<Footer />
</main>
)
}
+186
View File
@@ -0,0 +1,186 @@
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Privacy Policy - Sauci',
description: 'Privacy Policy for Sauci - the couples intimacy app',
}
export default function PrivacyPolicy() {
return (
<main className="min-h-screen bg-gradient-to-b from-background-light to-background">
{/* Header */}
<header className="py-6 px-6 border-b border-white/5">
<div className="max-w-4xl mx-auto flex items-center gap-4">
<Link href="/" className="flex items-center gap-2 text-white/60 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
<span>Back to Home</span>
</Link>
</div>
</header>
{/* Content */}
<div className="max-w-4xl mx-auto px-6 py-12">
<h1 className="text-4xl font-bold mb-2">Privacy Policy</h1>
<p className="text-white/40 mb-12">Last updated: December 2024</p>
<div className="prose prose-invert prose-lg max-w-none space-y-8">
<section>
<h2 className="text-2xl font-semibold text-white mb-4">1. Introduction</h2>
<p className="text-white/70 leading-relaxed">
Welcome to Sauci (&quot;we,&quot; &quot;our,&quot; or &quot;us&quot;). We are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our mobile application and related services (collectively, the &quot;Service&quot;).
</p>
<p className="text-white/70 leading-relaxed">
By using Sauci, you agree to the collection and use of information in accordance with this policy. If you do not agree with our policies and practices, please do not use our Service.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">2. Information We Collect</h2>
<h3 className="text-xl font-medium text-white/90 mb-3">2.1 Information You Provide</h3>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li><strong>Account Information:</strong> Email address, display name, and profile information when you create an account.</li>
<li><strong>Relationship Data:</strong> Information about your partner connection, including invite codes used to link accounts.</li>
<li><strong>Response Data:</strong> Your answers (yes/no/maybe) to questions within the app.</li>
<li><strong>Messages:</strong> Content of messages you send within match chat threads.</li>
<li><strong>Feedback:</strong> Any feedback, suggestions, or communications you send to us.</li>
</ul>
<h3 className="text-xl font-medium text-white/90 mb-3 mt-6">2.2 Automatically Collected Information</h3>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li><strong>Device Information:</strong> Device type, operating system, unique device identifiers.</li>
<li><strong>Usage Data:</strong> App interactions, features used, time spent in the app.</li>
<li><strong>Log Data:</strong> IP address, access times, and app crash reports.</li>
</ul>
<h3 className="text-xl font-medium text-white/90 mb-3 mt-6">2.3 Third-Party Services</h3>
<p className="text-white/70 leading-relaxed">
We use third-party services that may collect information:
</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li><strong>Authentication:</strong> Apple Sign-In for secure account creation.</li>
<li><strong>Analytics:</strong> To understand app usage and improve our service.</li>
<li><strong>Payment Processing:</strong> RevenueCat for subscription management (we do not store payment details).</li>
<li><strong>Error Tracking:</strong> Sentry for crash reporting and performance monitoring.</li>
</ul>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">3. How We Use Your Information</h2>
<p className="text-white/70 leading-relaxed mb-4">We use the collected information to:</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li>Provide, maintain, and improve our Service</li>
<li>Create and manage your account</li>
<li>Enable partner matching functionality</li>
<li>Process your question responses and determine matches</li>
<li>Facilitate in-app messaging between matched partners</li>
<li>Send push notifications about matches and messages</li>
<li>Process subscriptions and in-app purchases</li>
<li>Respond to your inquiries and provide customer support</li>
<li>Detect, prevent, and address technical issues and fraud</li>
<li>Analyze usage patterns to improve user experience</li>
</ul>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">4. Privacy of Your Responses</h2>
<div className="glass p-6 my-4">
<p className="text-white/90 font-medium mb-2">Important: Your Individual Answers Are Private</p>
<p className="text-white/70">
Your individual responses to questions are never shared with your partner or anyone else. Only when both partners respond positively (yes or maybe) to the same question is a &quot;match&quot; created and revealed to both users. This ensures that you can answer honestly without fear of judgment.
</p>
</div>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">5. Data Sharing and Disclosure</h2>
<p className="text-white/70 leading-relaxed mb-4">We do not sell your personal information. We may share information in the following circumstances:</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li><strong>With Your Partner:</strong> Match information (when both respond positively) and messages within chat threads.</li>
<li><strong>Service Providers:</strong> Third-party vendors who assist in operating our Service (hosting, analytics, payment processing).</li>
<li><strong>Legal Requirements:</strong> When required by law, legal process, or to protect our rights and safety.</li>
<li><strong>Business Transfers:</strong> In connection with a merger, acquisition, or sale of assets.</li>
</ul>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">6. Data Security</h2>
<p className="text-white/70 leading-relaxed">
We implement appropriate technical and organizational security measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction. This includes encryption of data in transit and at rest, secure authentication, and regular security assessments. However, no method of transmission over the Internet or electronic storage is 100% secure.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">7. Data Retention</h2>
<p className="text-white/70 leading-relaxed">
We retain your personal information for as long as your account is active or as needed to provide you services. If you delete your account, we will delete or anonymize your personal information within 30 days, except where we are required to retain it for legal purposes.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">8. Your Rights and Choices</h2>
<p className="text-white/70 leading-relaxed mb-4">You have the following rights regarding your data:</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li><strong>Access:</strong> Request a copy of the personal information we hold about you.</li>
<li><strong>Correction:</strong> Request correction of inaccurate personal information.</li>
<li><strong>Deletion:</strong> Request deletion of your account and associated data.</li>
<li><strong>Portability:</strong> Request a copy of your data in a portable format.</li>
<li><strong>Opt-out:</strong> Disable push notifications through your device settings.</li>
</ul>
<p className="text-white/70 leading-relaxed mt-4">
To exercise these rights, please contact us at <a href="mailto:privacy@sauci.app" className="text-primary hover:underline">privacy@sauci.app</a>.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">9. Children&apos;s Privacy</h2>
<p className="text-white/70 leading-relaxed">
Sauci is intended for users who are 18 years of age or older. We do not knowingly collect personal information from children under 18. If we learn that we have collected personal information from a child under 18, we will delete that information promptly.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">10. International Data Transfers</h2>
<p className="text-white/70 leading-relaxed">
Your information may be transferred to and processed in countries other than your country of residence. These countries may have different data protection laws. We ensure appropriate safeguards are in place to protect your information when transferred internationally.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">11. Changes to This Policy</h2>
<p className="text-white/70 leading-relaxed">
We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the &quot;Last updated&quot; date. You are advised to review this Privacy Policy periodically for any changes.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">12. Contact Us</h2>
<p className="text-white/70 leading-relaxed">
If you have any questions about this Privacy Policy or our data practices, please contact us at:
</p>
<div className="glass p-6 mt-4">
<p className="text-white/90">
<strong>Email:</strong>{' '}
<a href="mailto:privacy@sauci.app" className="text-primary hover:underline">privacy@sauci.app</a>
</p>
</div>
</section>
</div>
</div>
{/* Footer */}
<footer className="py-8 px-6 border-t border-white/5">
<div className="max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<span className="text-xl font-bold gradient-text">Sauci</span>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<Link href="/" className="hover:text-white transition-colors">Home</Link>
</div>
</div>
</footer>
</main>
)
}
+247
View File
@@ -0,0 +1,247 @@
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Terms & Conditions - Sauci',
description: 'Terms and Conditions for Sauci - the couples intimacy app',
}
export default function TermsAndConditions() {
return (
<main className="min-h-screen bg-gradient-to-b from-background-light to-background">
{/* Header */}
<header className="py-6 px-6 border-b border-white/5">
<div className="max-w-4xl mx-auto flex items-center gap-4">
<Link href="/" className="flex items-center gap-2 text-white/60 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
<span>Back to Home</span>
</Link>
</div>
</header>
{/* Content */}
<div className="max-w-4xl mx-auto px-6 py-12">
<h1 className="text-4xl font-bold mb-2">Terms & Conditions</h1>
<p className="text-white/40 mb-12">Last updated: December 2024</p>
<div className="prose prose-invert prose-lg max-w-none space-y-8">
<section>
<h2 className="text-2xl font-semibold text-white mb-4">1. Agreement to Terms</h2>
<p className="text-white/70 leading-relaxed">
By accessing or using Sauci (&quot;the App&quot;), you agree to be bound by these Terms and Conditions (&quot;Terms&quot;). If you disagree with any part of these terms, you may not access or use our Service.
</p>
<p className="text-white/70 leading-relaxed">
These Terms constitute a legally binding agreement between you and Sauci regarding your use of the App and any related services.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">2. Eligibility</h2>
<p className="text-white/70 leading-relaxed">
You must be at least 18 years of age to use Sauci. By using the App, you represent and warrant that:
</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li>You are at least 18 years old</li>
<li>You have the legal capacity to enter into these Terms</li>
<li>You are not prohibited from using the App under applicable laws</li>
<li>You will use the App in compliance with all applicable laws and regulations</li>
</ul>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">3. Account Registration</h2>
<p className="text-white/70 leading-relaxed mb-4">
To use certain features of Sauci, you must create an account. When creating an account, you agree to:
</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li>Provide accurate, current, and complete information</li>
<li>Maintain the security of your account credentials</li>
<li>Notify us immediately of any unauthorized access to your account</li>
<li>Accept responsibility for all activities that occur under your account</li>
</ul>
<p className="text-white/70 leading-relaxed mt-4">
We reserve the right to suspend or terminate your account if any information provided is inaccurate, false, or violates these Terms.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">4. Partner Connection</h2>
<p className="text-white/70 leading-relaxed">
Sauci is designed for use between consenting adult partners. By connecting with another user:
</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li>You confirm that you have their consent to connect</li>
<li>You understand that matches will be visible to both parties</li>
<li>You agree to use the messaging features respectfully</li>
<li>You acknowledge that either party can end the connection at any time</li>
</ul>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">5. Acceptable Use</h2>
<p className="text-white/70 leading-relaxed mb-4">
You agree not to use Sauci to:
</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li>Harass, abuse, or harm another person</li>
<li>Share content that is illegal, harmful, threatening, or violates others&apos; rights</li>
<li>Impersonate any person or entity</li>
<li>Collect or store personal data about other users without consent</li>
<li>Interfere with or disrupt the App or servers</li>
<li>Attempt to gain unauthorized access to the App or other accounts</li>
<li>Use the App for any commercial purpose without authorization</li>
<li>Violate any applicable laws or regulations</li>
<li>Share explicit content involving minors or non-consensual activities</li>
</ul>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">6. Content and Messaging</h2>
<p className="text-white/70 leading-relaxed">
You retain ownership of any content you submit through the App. By submitting content, you grant us a non-exclusive, royalty-free license to use, store, and display such content solely for the purpose of providing the Service.
</p>
<p className="text-white/70 leading-relaxed mt-4">
Messages sent within match chat threads are private between you and your connected partner. We do not actively monitor message content but reserve the right to review content if reported for violations of these Terms.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">7. Subscriptions and Payments</h2>
<h3 className="text-xl font-medium text-white/90 mb-3">7.1 Premium Features</h3>
<p className="text-white/70 leading-relaxed">
Sauci offers premium features through subscription plans. Subscription fees are charged through the Apple App Store. All purchases are subject to Apple&apos;s terms and conditions.
</p>
<h3 className="text-xl font-medium text-white/90 mb-3 mt-6">7.2 Billing</h3>
<p className="text-white/70 leading-relaxed">
Subscriptions automatically renew unless cancelled at least 24 hours before the end of the current billing period. You can manage your subscription through your Apple ID account settings.
</p>
<h3 className="text-xl font-medium text-white/90 mb-3 mt-6">7.3 Refunds</h3>
<p className="text-white/70 leading-relaxed">
Refund requests are handled by Apple in accordance with their refund policies. We do not have direct access to process refunds for App Store purchases.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">8. Intellectual Property</h2>
<p className="text-white/70 leading-relaxed">
The App and its original content (excluding user-submitted content), features, and functionality are owned by Sauci and are protected by international copyright, trademark, patent, trade secret, and other intellectual property laws.
</p>
<p className="text-white/70 leading-relaxed mt-4">
You may not copy, modify, distribute, sell, or lease any part of our App or included content, nor may you reverse engineer or attempt to extract the source code.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">9. Disclaimers</h2>
<div className="glass p-6 my-4">
<p className="text-white/70">
THE APP 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>
</div>
<p className="text-white/70 leading-relaxed">
We do not warrant that the App will be uninterrupted, timely, secure, or error-free. We do not make any guarantees regarding the results that may be obtained from using the App or the accuracy of any information obtained through the App.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">10. Limitation of Liability</h2>
<p className="text-white/70 leading-relaxed">
To the maximum extent permitted by applicable law, Sauci shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses resulting from:
</p>
<ul className="list-disc pl-6 text-white/70 space-y-2">
<li>Your use or inability to use the App</li>
<li>Any unauthorized access to or use of our servers or personal information</li>
<li>Any interruption or cessation of transmission to or from the App</li>
<li>Any bugs, viruses, or similar issues transmitted through the App</li>
<li>Any content or conduct of any third party on the App</li>
</ul>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">11. Indemnification</h2>
<p className="text-white/70 leading-relaxed">
You agree to defend, indemnify, and hold harmless Sauci and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, and expenses, including reasonable attorney&apos;s fees, arising out of or relating to your use of the App or your violation of these Terms.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">12. Termination</h2>
<p className="text-white/70 leading-relaxed">
We may terminate or suspend your account and access to the App immediately, without prior notice or liability, for any reason, including if you breach these Terms.
</p>
<p className="text-white/70 leading-relaxed mt-4">
Upon termination, your right to use the App will immediately cease. If you wish to terminate your account, you may do so through the App settings or by contacting us.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">13. Changes to Terms</h2>
<p className="text-white/70 leading-relaxed">
We reserve the right to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days&apos; notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.
</p>
<p className="text-white/70 leading-relaxed mt-4">
By continuing to access or use our App after those revisions become effective, you agree to be bound by the revised terms.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">14. Governing Law</h2>
<p className="text-white/70 leading-relaxed">
These Terms shall be governed by and construed in accordance with the laws of the jurisdiction in which Sauci operates, without regard to its conflict of law provisions.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">15. Dispute Resolution</h2>
<p className="text-white/70 leading-relaxed">
Any dispute arising from or relating to these Terms or the App shall first be attempted to be resolved through good-faith negotiations. If negotiations fail, disputes shall be resolved through binding arbitration in accordance with applicable arbitration rules.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">16. Severability</h2>
<p className="text-white/70 leading-relaxed">
If any provision of these Terms is held to be invalid or unenforceable, such provision shall be struck and the remaining provisions shall be enforced to the fullest extent under law.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">17. Entire Agreement</h2>
<p className="text-white/70 leading-relaxed">
These Terms, together with our Privacy Policy, constitute the entire agreement between you and Sauci regarding your use of the App and supersede any prior agreements.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">18. Contact Us</h2>
<p className="text-white/70 leading-relaxed">
If you have any questions about these Terms, please contact us at:
</p>
<div className="glass p-6 mt-4">
<p className="text-white/90">
<strong>Email:</strong>{' '}
<a href="mailto:legal@sauci.app" className="text-primary hover:underline">legal@sauci.app</a>
</p>
</div>
</section>
</div>
</div>
{/* Footer */}
<footer className="py-8 px-6 border-t border-white/5">
<div className="max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<span className="text-xl font-bold gradient-text">Sauci</span>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<Link href="/" className="hover:text-white transition-colors">Home</Link>
</div>
</div>
</footer>
</main>
)
}
+60
View File
@@ -0,0 +1,60 @@
'use client'
import Image from 'next/image'
import { motion } from 'framer-motion'
export default function CTA() {
return (
<section className="relative py-24 overflow-hidden">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-b from-background to-background-light" />
{/* Accent Glows */}
<div className="absolute top-0 left-1/4 w-[500px] h-[500px] bg-primary/20 rounded-full blur-[150px]" />
<div className="absolute bottom-0 right-1/4 w-[400px] h-[400px] bg-secondary/20 rounded-full blur-[150px]" />
<div className="relative z-10 max-w-4xl mx-auto px-6 text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<div className="inline-flex items-center justify-center mb-8">
<Image
src="/logo.png"
alt="Sauci"
width={100}
height={100}
className="w-24 h-24 drop-shadow-[0_0_30px_rgba(233,69,96,0.5)]"
/>
</div>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-6">
Ready to Discover What You{' '}
<span className="gradient-text">Both Desire?</span>
</h2>
<p className="text-lg text-white/60 max-w-2xl mx-auto mb-8">
Join thousands of couples who have deepened their connection with Sauci.
Download now and start exploring together.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a
href="https://apps.apple.com/app/sauci"
target="_blank"
rel="noopener noreferrer"
className="btn-primary flex items-center gap-2"
>
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
</svg>
Download for iOS
</a>
<span className="text-white/40">Coming soon to Android</span>
</div>
</motion.div>
</div>
</section>
)
}
+94
View File
@@ -0,0 +1,94 @@
'use client'
import { motion } from 'framer-motion'
import { Heart, MessageCircle, Lock, Sparkles, Users, Flame } from 'lucide-react'
const features = [
{
icon: Heart,
title: 'Swipe to Connect',
description: 'Answer intimate questions with simple swipes. Yes, no, or maybe - your desires stay private until you match.',
color: 'text-primary',
bgColor: 'bg-primary/10',
},
{
icon: Sparkles,
title: 'Discover Matches',
description: 'When both partners swipe positively on a question, a match is created. Discover what you both secretly want.',
color: 'text-secondary',
bgColor: 'bg-secondary/10',
},
{
icon: MessageCircle,
title: 'Unlock Conversations',
description: 'Each match opens a private chat thread. Talk about your desires in a safe, dedicated space.',
color: 'text-green-400',
bgColor: 'bg-green-400/10',
},
{
icon: Lock,
title: 'Complete Privacy',
description: 'Your individual answers are never revealed. Only mutual matches are shown to both partners.',
color: 'text-blue-400',
bgColor: 'bg-blue-400/10',
},
{
icon: Flame,
title: 'Curated Question Packs',
description: 'Explore themed packs from playful to passionate. New packs added regularly to keep things exciting.',
color: 'text-orange-400',
bgColor: 'bg-orange-400/10',
},
{
icon: Users,
title: 'Built for Couples',
description: 'Designed exclusively for partners. Connect with your significant other using a private invite code.',
color: 'text-pink-400',
bgColor: 'bg-pink-400/10',
},
]
export default function Features() {
return (
<section id="features" className="relative py-24 overflow-hidden">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-b from-background to-background-light" />
<div className="relative z-10 max-w-7xl mx-auto px-6">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-4">
Everything You Need to{' '}
<span className="gradient-text">Explore Together</span>
</h2>
<p className="text-lg text-white/60 max-w-2xl mx-auto">
Sauci creates a safe space for couples to discover their shared desires without fear of judgment.
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feature, index) => (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="glass p-6 hover:border-white/20 transition-all duration-300 group"
>
<div className={`w-14 h-14 rounded-2xl ${feature.bgColor} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`}>
<feature.icon className={`w-7 h-7 ${feature.color}`} />
</div>
<h3 className="text-xl font-semibold mb-2">{feature.title}</h3>
<p className="text-white/60">{feature.description}</p>
</motion.div>
))}
</div>
</div>
</section>
)
}
+46
View File
@@ -0,0 +1,46 @@
'use client'
import Link from 'next/link'
import Image from 'next/image'
export default function Footer() {
const currentYear = new Date().getFullYear()
return (
<footer className="relative py-12 border-t border-white/5">
<div className="max-w-7xl mx-auto px-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
{/* Logo */}
<Link href="/" className="flex items-center gap-2">
<Image
src="/logo.png"
alt="Sauci"
width={32}
height={32}
className="w-8 h-8"
/>
<span className="text-xl font-bold gradient-text">Sauci</span>
</Link>
{/* Links */}
<div className="flex flex-wrap items-center justify-center gap-6">
<Link href="/privacy" className="text-white/60 hover:text-white transition-colors text-sm">
Privacy Policy
</Link>
<Link href="/terms" className="text-white/60 hover:text-white transition-colors text-sm">
Terms & Conditions
</Link>
<a href="mailto:support@sauci.app" className="text-white/60 hover:text-white transition-colors text-sm">
Contact
</a>
</div>
{/* Copyright */}
<div className="text-white/40 text-sm">
&copy; {currentYear} Sauci. All rights reserved.
</div>
</div>
</div>
</footer>
)
}
+170
View File
@@ -0,0 +1,170 @@
'use client'
import Image from 'next/image'
import { motion } from 'framer-motion'
import { Sparkles } from 'lucide-react'
export default function Hero() {
return (
<section className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20">
{/* Background Effects */}
<div className="absolute inset-0 bg-gradient-to-b from-background-light to-background" />
{/* Accent Glows */}
<div className="absolute top-20 right-0 w-[600px] h-[600px] bg-primary/20 rounded-full blur-[150px] animate-pulse-glow" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-secondary/20 rounded-full blur-[150px] animate-pulse-glow" style={{ animationDelay: '2s' }} />
<div className="relative z-10 max-w-7xl mx-auto px-6 py-20 flex flex-col lg:flex-row items-center gap-12 lg:gap-16">
{/* Text Content */}
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
className="flex-1 text-center lg:text-left"
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="inline-flex items-center gap-2 glass px-4 py-2 mb-6"
>
<Sparkles className="w-4 h-4 text-primary" />
<span className="text-sm text-white/80">Discover What You Both Desire</span>
</motion.div>
<h1 className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold leading-tight mb-6">
Ignite Your{' '}
<span className="gradient-text">Connection</span>
</h1>
<p className="text-lg sm:text-xl text-white/60 max-w-xl mx-auto lg:mx-0 mb-8">
Sauci helps couples explore their desires through playful questions.
Swipe, match, and unlock deeper conversations with your partner.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4">
<a
href="https://apps.apple.com/app/sauci"
target="_blank"
rel="noopener noreferrer"
className="btn-primary flex items-center gap-2 w-full sm:w-auto justify-center"
>
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
</svg>
Download for iOS
</a>
<a href="#how-it-works" className="btn-secondary flex items-center gap-2 w-full sm:w-auto justify-center">
Learn More
</a>
</div>
</motion.div>
{/* Phone Mockups */}
<motion.div
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="flex-1 flex justify-center items-center"
>
<div className="relative flex items-center justify-center">
{/* Glow behind phones */}
<div className="absolute w-[400px] h-[500px] bg-gradient-primary rounded-full blur-[100px] opacity-20" />
{/* Back Phone - Packs */}
<motion.div
initial={{ opacity: 0, x: 60, rotate: 12 }}
animate={{ opacity: 1, x: 60, rotate: 12 }}
transition={{ duration: 0.8, delay: 0.6 }}
className="absolute -right-4 sm:right-0"
>
<PhoneMockup
screenshot="/screenshots/packs.png"
alt="Question Packs"
className="w-[180px] sm:w-[220px] opacity-60"
/>
</motion.div>
{/* Back Phone - Swipe */}
<motion.div
initial={{ opacity: 0, x: -60, rotate: -12 }}
animate={{ opacity: 1, x: -60, rotate: -12 }}
transition={{ duration: 0.8, delay: 0.5 }}
className="absolute -left-4 sm:left-0"
>
<PhoneMockup
screenshot="/screenshots/swipe.png"
alt="Swipe on Questions"
className="w-[180px] sm:w-[220px] opacity-60"
/>
</motion.div>
{/* Front Phone - Home */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
className="relative z-10 animate-float"
>
<PhoneMockup
screenshot="/screenshots/home.png"
alt="Sauci Home Screen"
className="w-[220px] sm:w-[280px]"
featured
/>
</motion.div>
</div>
</motion.div>
</div>
{/* Scroll Indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5 }}
className="absolute bottom-8 left-1/2 -translate-x-1/2"
>
<div className="w-6 h-10 rounded-full border-2 border-white/20 flex justify-center pt-2">
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ repeat: Infinity, duration: 1.5 }}
className="w-1.5 h-1.5 rounded-full bg-white/60"
/>
</div>
</motion.div>
</section>
)
}
function PhoneMockup({
screenshot,
alt,
className = '',
featured = false,
}: {
screenshot: string
alt: string
className?: string
featured?: boolean
}) {
return (
<div className={`relative ${className}`}>
{/* Phone Frame */}
<div className={`relative bg-black rounded-[2.5rem] p-2 shadow-2xl ${featured ? 'glow-primary' : ''}`}>
{/* Screen */}
<div className="relative rounded-[2rem] overflow-hidden bg-background">
<Image
src={screenshot}
alt={alt}
width={430}
height={932}
className="w-full h-auto"
priority={featured}
/>
</div>
{/* Dynamic Island / Notch */}
<div className="absolute top-3 left-1/2 -translate-x-1/2 w-[30%] h-[3%] bg-black rounded-full" />
</div>
</div>
)
}
+80
View File
@@ -0,0 +1,80 @@
'use client'
import { motion } from 'framer-motion'
const steps = [
{
number: '01',
title: 'Connect with Your Partner',
description: 'Create your account and invite your partner using a unique code. Your journey begins together.',
},
{
number: '02',
title: 'Browse Question Packs',
description: 'Choose from curated packs ranging from romantic to adventurous. Each pack contains thoughtful questions.',
},
{
number: '03',
title: 'Swipe on Questions',
description: 'Answer each question honestly. Swipe right for yes, left for no, or up for maybe. Your answers stay private.',
},
{
number: '04',
title: 'Discover Your Matches',
description: 'When both partners answer positively, it\'s a match! Explore your shared desires through private chats.',
},
]
export default function HowItWorks() {
return (
<section id="how-it-works" className="relative py-24 overflow-hidden">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-b from-background-light to-background" />
{/* Accent Glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-secondary/10 rounded-full blur-[200px]" />
<div className="relative z-10 max-w-7xl mx-auto px-6">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-4">
How <span className="gradient-text">Sauci</span> Works
</h2>
<p className="text-lg text-white/60 max-w-2xl mx-auto">
Four simple steps to deeper connection and discovery with your partner.
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{steps.map((step, index) => (
<motion.div
key={step.number}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.15 }}
className="relative"
>
{/* Connector Line */}
{index < steps.length - 1 && (
<div className="hidden lg:block absolute top-12 left-[60%] w-full h-0.5 bg-gradient-to-r from-primary/50 to-transparent" />
)}
<div className="glass p-6 h-full">
<div className="text-5xl font-bold gradient-text mb-4 opacity-50">
{step.number}
</div>
<h3 className="text-xl font-semibold mb-2">{step.title}</h3>
<p className="text-white/60">{step.description}</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
)
}
+103
View File
@@ -0,0 +1,103 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { motion } from 'framer-motion'
import { Menu, X } from 'lucide-react'
export default function Navbar() {
const [isScrolled, setIsScrolled] = useState(false)
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
return (
<motion.nav
initial={{ y: -100 }}
animate={{ y: 0 }}
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled ? 'glass py-4' : 'py-6'
}`}
>
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
<Link href="/" className="flex items-center gap-3">
<Image
src="/logo.png"
alt="Sauci"
width={40}
height={40}
className="w-10 h-10"
/>
<span className="text-2xl font-bold gradient-text">Sauci</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-8">
<a href="#features" className="text-white/70 hover:text-white transition-colors">
Features
</a>
<a href="#how-it-works" className="text-white/70 hover:text-white transition-colors">
How It Works
</a>
<a
href="https://apps.apple.com/app/sauci"
target="_blank"
rel="noopener noreferrer"
className="btn-primary text-sm py-3 px-6"
>
Download App
</a>
</div>
{/* Mobile Menu Button */}
<button
className="md:hidden text-white"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
{/* Mobile Menu */}
{isMobileMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="md:hidden glass mt-4 mx-6 p-6"
>
<div className="flex flex-col gap-4">
<a
href="#features"
className="text-white/70 hover:text-white transition-colors"
onClick={() => setIsMobileMenuOpen(false)}
>
Features
</a>
<a
href="#how-it-works"
className="text-white/70 hover:text-white transition-colors"
onClick={() => setIsMobileMenuOpen(false)}
>
How It Works
</a>
<a
href="https://apps.apple.com/app/sauci"
target="_blank"
rel="noopener noreferrer"
className="btn-primary text-sm py-3 px-6 text-center"
>
Download App
</a>
</div>
</motion.div>
)}
</motion.nav>
)
}
+17
View File
@@ -0,0 +1,17 @@
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
environment:
- NODE_ENV=production
- NEXT_TELEMETRY_DISABLED=1
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
+19
View File
@@ -0,0 +1,19 @@
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "3005:3000"
environment:
- NODE_ENV=production
- NEXT_TELEMETRY_DISABLED=1
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfig
+1670
View File
File diff suppressed because it is too large. Load diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@sauci/web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "^14.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"framer-motion": "^11.0.0",
"lucide-react": "^0.460.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#e94560"/>
<stop offset="100%" style="stop-color:#9b59b6"/>
</linearGradient>
</defs>
<circle cx="50" cy="50" r="45" fill="url(#gradient)"/>
<path d="M50 25 C35 25 25 38 25 50 C25 70 50 80 50 80 C50 80 75 70 75 50 C75 38 65 25 50 25 Z" fill="white" opacity="0.9"/>
</svg>

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

+34
View File
@@ -0,0 +1,34 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
background: '#0d0d1a',
'background-light': '#1a1a2e',
surface: 'rgba(22, 33, 62, 0.6)',
primary: '#e94560',
'primary-dark': '#c73a52',
secondary: '#9b59b6',
'secondary-dark': '#8e44ad',
glass: {
DEFAULT: 'rgba(22, 33, 62, 0.4)',
light: 'rgba(22, 33, 62, 0.6)',
border: 'rgba(255, 255, 255, 0.08)',
'border-light': 'rgba(255, 255, 255, 0.12)',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-primary': 'linear-gradient(135deg, #e94560 0%, #9b59b6 100%)',
},
},
},
plugins: [],
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
View File
Whitespace-only changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

+11290 -174
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -3,6 +3,7 @@
"private": true,
"workspaces": [
"apps/admin",
"apps/mobile",
"apps/supabase",
"packages/*"
],