product
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env*
|
||||
.cursor
|
||||
*.md
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
package-lock.json
|
||||
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
47
App.tsx
Normal file
47
App.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Navbar from './components/Navbar';
|
||||
import Hero from './components/Hero';
|
||||
import Features from './components/Features';
|
||||
import HowItWorks from './components/HowItWorks';
|
||||
import CallToAction from './components/CallToAction';
|
||||
import Footer from './components/Footer';
|
||||
import PrivacyPolicy from './components/PrivacyPolicy';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [currentPage, setCurrentPage] = useState<'home' | 'privacy'>('home');
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [currentPage]);
|
||||
|
||||
const navigateToPrivacy = (e?: React.MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
setCurrentPage('privacy');
|
||||
};
|
||||
|
||||
const navigateToHome = (e?: React.MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
setCurrentPage('home');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background-dark text-text-light font-sans min-h-screen selection:bg-primary selection:text-background-dark">
|
||||
<Navbar onLogoClick={navigateToHome} />
|
||||
<main>
|
||||
{currentPage === 'home' ? (
|
||||
<>
|
||||
<Hero />
|
||||
<Features />
|
||||
<HowItWorks />
|
||||
<CallToAction />
|
||||
</>
|
||||
) : (
|
||||
<PrivacyPolicy onBack={navigateToHome} />
|
||||
)}
|
||||
</main>
|
||||
<Footer onPrivacyClick={navigateToPrivacy} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Build the app
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
# Copy built assets from build stage
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy custom nginx config
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/1h-948Wo2X25UbJgBUL1cwCuXWy0zsm7v
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
BIN
assets/images/icon.png
Normal file
BIN
assets/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
53
components/CallToAction.tsx
Normal file
53
components/CallToAction.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Mail } from 'lucide-react';
|
||||
|
||||
const CallToAction: React.FC = () => {
|
||||
return (
|
||||
<section className="py-20 bg-background-dark">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="relative rounded-3xl overflow-hidden bg-primary text-gray-900 p-12 text-center shadow-2xl">
|
||||
{/* Subtle patterned overlay */}
|
||||
<div className="absolute inset-0 opacity-10 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-white via-transparent to-transparent"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-3xl md:text-5xl font-display font-bold mb-6">
|
||||
Ready to boost your focus?
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl text-gray-900/80 mb-10 max-w-2xl mx-auto">
|
||||
Join thousands of students and lifelong learners mastering new topics every day with Nemia.
|
||||
</p>
|
||||
|
||||
<form
|
||||
action="https://getlaunchlist.com/s/pAqdup"
|
||||
method="POST"
|
||||
className="flex flex-col sm:flex-row justify-center gap-3 max-w-lg mx-auto"
|
||||
>
|
||||
<div className="relative flex-grow">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={20} />
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
className="w-full bg-gray-900/10 border border-gray-900/20 rounded-xl py-4 pl-12 pr-4 text-gray-900 placeholder:text-gray-900/50 focus:outline-none focus:border-gray-900 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-gray-900 text-white px-8 py-4 rounded-xl font-bold text-lg hover:scale-105 transition-transform flex items-center justify-center gap-2 whitespace-nowrap"
|
||||
>
|
||||
<span>Join Waitlist</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-6 text-sm font-medium opacity-60 text-gray-900">
|
||||
No credit card required.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallToAction;
|
||||
56
components/Features.tsx
Normal file
56
components/Features.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { Smartphone, Sparkles, Clock } from 'lucide-react';
|
||||
|
||||
const Features: React.FC = () => {
|
||||
return (
|
||||
<section id="features" className="py-24 bg-background-dark">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center max-w-3xl mx-auto mb-16">
|
||||
<h2 className="text-3xl lg:text-4xl font-display font-bold mb-4 text-white">
|
||||
Designed for Radical Retention
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg">
|
||||
We combine the science of Spaced Repetition with the psychology of habit formation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{/* Feature 1 */}
|
||||
<div className="bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-indigo-900/30 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<Smartphone className="text-indigo-400" size={30} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-3 text-white">Focus Mode (Android)</h3>
|
||||
<p className="text-gray-400 leading-relaxed">
|
||||
Blocks social media and games when cards are due. Complete your daily goal to unlock them automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 2 */}
|
||||
<div className="bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-primary/20 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<Sparkles className="text-emerald-400" size={30} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-3 text-white">AI Generation</h3>
|
||||
<p className="text-gray-400 leading-relaxed">
|
||||
Generate decks using ChatGPT, Claude, or Gemini. Copy custom prompts and import JSON cards instantly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 3 */}
|
||||
<div className="bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-pink-900/30 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<Clock className="text-pink-400" size={30} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-3 text-white">Cloud Sync & Offline</h3>
|
||||
<p className="text-gray-400 leading-relaxed">
|
||||
Study anywhere. Your decks and progress sync across devices automatically with full offline support.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Features;
|
||||
56
components/Footer.tsx
Normal file
56
components/Footer.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { Twitter } from 'lucide-react';
|
||||
|
||||
interface FooterProps {
|
||||
onPrivacyClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const Footer: React.FC<FooterProps> = ({ onPrivacyClick }) => {
|
||||
return (
|
||||
<footer className="bg-[#0A0C12] pt-16 pb-8 border-t border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-2 gap-8 mb-12">
|
||||
{/* Brand Column */}
|
||||
<div className="col-span-1">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<img
|
||||
src="/assets/images/icon.png"
|
||||
alt="Nemia Logo"
|
||||
className="w-6 h-6 rounded object-contain"
|
||||
/>
|
||||
<span className="font-display font-bold text-lg text-white">
|
||||
Nemia
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 max-w-xs">
|
||||
Making learning inevitable through smart blocking and spaced repetition.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Legal Links */}
|
||||
<div className="flex flex-col items-end">
|
||||
<h4 className="font-bold text-white mb-4">Legal</h4>
|
||||
<ul className="space-y-2 text-sm text-gray-400 text-right">
|
||||
<li><a href="#" onClick={onPrivacyClick} className="hover:text-primary transition-colors">Privacy</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Terms</a></li>
|
||||
<li><a href="#" className="hover:text-primary transition-colors">Cookie Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-sm text-gray-400">© 2026 Nemia Inc. All rights reserved.</p>
|
||||
<div className="flex space-x-6">
|
||||
<a href="#" className="text-gray-400 hover:text-primary transition-colors">
|
||||
<span className="sr-only">Twitter</span>
|
||||
<Twitter size={20} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
114
components/Hero.tsx
Normal file
114
components/Hero.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import { Mail, Lock, Check } from 'lucide-react';
|
||||
|
||||
const Hero: React.FC = () => {
|
||||
return (
|
||||
<header className="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden mesh-gradient">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
|
||||
{/* Left Content */}
|
||||
<div className="space-y-8 text-center lg:text-left">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-semibold uppercase tracking-wider">
|
||||
<span className="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
|
||||
New: AI Deck Generation
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl lg:text-7xl font-display font-bold leading-tight tracking-tight text-white">
|
||||
Unlock Apps with <br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-emerald-400">
|
||||
Knowledge
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-gray-400 max-w-2xl mx-auto lg:mx-0 leading-relaxed">
|
||||
Stop doom-scrolling and start learning. Nemia blocks distracting apps on Android until you complete your daily study goals. Master anything with scientifically-backed spaced repetition and cloud sync.
|
||||
</p>
|
||||
|
||||
<form
|
||||
action="https://getlaunchlist.com/s/pAqdup"
|
||||
method="POST"
|
||||
className="flex flex-col sm:flex-row gap-3 justify-center lg:justify-start max-w-md mx-auto lg:mx-0"
|
||||
>
|
||||
<div className="relative flex-grow">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" size={20} />
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
className="w-full bg-surface-dark border border-gray-700 rounded-xl py-4 pl-12 pr-4 text-white focus:outline-none focus:border-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-primary text-gray-900 px-8 py-4 rounded-xl font-bold text-lg hover:brightness-110 transition-all shadow-glow flex items-center justify-center gap-2 whitespace-nowrap"
|
||||
>
|
||||
<span>Join Waitlist</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Right Visual (Mockups) */}
|
||||
<div className="relative mx-auto w-full max-w-md lg:max-w-full">
|
||||
{/* Background Glows */}
|
||||
<div className="absolute -top-10 -right-10 w-72 h-72 bg-purple-500/20 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-10 -left-10 w-72 h-72 bg-primary/20 rounded-full blur-3xl"></div>
|
||||
|
||||
{/* Main Focus Mode Card */}
|
||||
<div className="relative bg-surface-dark border border-gray-700 rounded-3xl p-6 shadow-2xl transform rotate-3 hover:rotate-0 transition-transform duration-500">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h3 className="text-xl font-bold text-white">Focus Mode</h3>
|
||||
<div className="bg-red-500/10 text-red-500 px-3 py-1 rounded-full text-xs font-bold flex items-center gap-1">
|
||||
<Lock size={14} /> Focus Mode (Android)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-6xl font-display font-bold text-white mb-2">
|
||||
<span className="text-white">22</span>
|
||||
</div>
|
||||
<p className="text-gray-400 font-medium">Cards Due</p>
|
||||
<p className="text-sm text-gray-500">(+3 tomorrow)</p>
|
||||
</div>
|
||||
|
||||
<button className="w-full bg-primary text-gray-900 font-bold py-4 rounded-xl shadow-lg hover:shadow-glow transition-all mb-6">
|
||||
Review Now to Unlock
|
||||
</button>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="bg-surface-accent p-4 rounded-xl flex justify-between items-center border border-gray-700/50">
|
||||
<div>
|
||||
<p className="font-semibold text-sm text-white">Korean Vocab</p>
|
||||
<p className="text-xs text-gray-500">41 Cards total</p>
|
||||
</div>
|
||||
<span className="text-primary text-sm font-bold">18 Due</span>
|
||||
</div>
|
||||
<div className="bg-surface-accent p-4 rounded-xl flex justify-between items-center border border-gray-700/50">
|
||||
<div>
|
||||
<p className="font-semibold text-sm text-white">Russian Vocab</p>
|
||||
<p className="text-xs text-gray-500">67 Cards total</p>
|
||||
</div>
|
||||
<span className="text-gray-500 text-sm font-bold">0 Due</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Streak Badge (Floating) */}
|
||||
<div className="absolute -bottom-6 -left-6 bg-surface-accent text-white p-4 rounded-xl shadow-xl flex items-center gap-3 animate-bounce border border-gray-700 z-20">
|
||||
<div className="bg-green-500 rounded-full p-1">
|
||||
<Check size={14} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-wide opacity-70">Streak</p>
|
||||
<p className="font-bold text-lg leading-none">14 Days</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
116
components/HowItWorks.tsx
Normal file
116
components/HowItWorks.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { MoreHorizontal, ChevronRight, Plus, Globe, Book } from 'lucide-react';
|
||||
|
||||
const HowItWorks: React.FC = () => {
|
||||
return (
|
||||
<section id="how-it-works" className="py-24 bg-[#0A0C12] overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
|
||||
{/* Text Content */}
|
||||
<div className="order-2 lg:order-1">
|
||||
<h2 className="text-3xl lg:text-4xl font-display font-bold mb-6 text-white">
|
||||
Your Pocket Tutor. <br />
|
||||
<span className="text-primary">Always Ready.</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-8 mt-10">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-blue-900/30 flex items-center justify-center text-blue-400 font-bold">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-white mb-2">Build Your Library</h4>
|
||||
<p className="text-gray-400">Create decks manually, use AI prompts, or import shared decks from friends via unique codes.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-emerald-400 font-bold">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-white mb-2">Configure Focus Mode</h4>
|
||||
<p className="text-gray-400">On Android, select distracting apps to block. They'll stay locked until you complete your daily reviews.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-purple-900/30 flex items-center justify-center text-purple-400 font-bold">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-bold text-white mb-2">Study & Sync</h4>
|
||||
<p className="text-gray-400">Review cards using SRS. Your progress syncs to the cloud automatically, even after studying offline.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual Mockups */}
|
||||
<div className="order-1 lg:order-2 relative">
|
||||
<div className="relative w-full aspect-square max-w-md mx-auto">
|
||||
|
||||
{/* Back Card Ghost */}
|
||||
<div className="absolute top-0 right-0 w-3/4 bg-surface-dark opacity-30 rounded-2xl h-64 transform rotate-6 translate-x-4 border border-gray-700"></div>
|
||||
|
||||
{/* Main Flashcard */}
|
||||
<div className="absolute top-8 right-8 w-3/4 bg-surface-dark rounded-2xl shadow-2xl p-6 border border-gray-700 transform -rotate-3 z-10">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<span className="text-gray-400 text-xs uppercase tracking-widest">Korean Vocab</span>
|
||||
<MoreHorizontal size={20} className="text-gray-400" />
|
||||
</div>
|
||||
<div className="h-24 flex items-center justify-center text-center">
|
||||
<h3 className="text-2xl font-bold text-white">억울하다</h3>
|
||||
</div>
|
||||
<div className="border-t border-gray-700 my-4"></div>
|
||||
<p className="text-gray-300 text-center mb-6">To feel wronged / To feel unfair</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button className="bg-red-500/20 text-red-400 py-2 rounded-lg text-sm font-medium hover:bg-red-500/30 transition-colors">Again</button>
|
||||
<button className="bg-orange-500/20 text-orange-400 py-2 rounded-lg text-sm font-medium hover:bg-orange-500/30 transition-colors">Hard</button>
|
||||
<button className="bg-green-500/20 text-green-400 py-2 rounded-lg text-sm font-medium hover:bg-green-500/30 transition-colors">Good</button>
|
||||
<button className="bg-primary/20 text-primary py-2 rounded-lg text-sm font-medium hover:bg-primary/30 transition-colors">Easy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* My Decks List Card (Overlapping) */}
|
||||
<div className="absolute bottom-0 left-0 w-3/4 bg-surface-dark rounded-2xl shadow-2xl p-5 border border-gray-700 z-20">
|
||||
<h4 className="font-bold text-lg mb-4 text-white">My Decks</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-gradient-to-r from-purple-500/10 to-blue-500/10 p-3 rounded-lg border border-purple-500/20 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Book size={20} className="text-purple-400" />
|
||||
<div>
|
||||
<p className="font-bold text-sm text-white">Maths</p>
|
||||
<p className="text-xs text-gray-500">4 cards • <span className="text-primary">2 due</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-gray-400" />
|
||||
</div>
|
||||
<div className="bg-gradient-to-r from-emerald-500/10 to-teal-500/10 p-3 rounded-lg border border-emerald-500/20 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe size={20} className="text-emerald-400" />
|
||||
<div>
|
||||
<p className="font-bold text-sm text-white">Geography</p>
|
||||
<p className="text-xs text-gray-500">2 cards • <span className="text-primary">2 due</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-5 -right-5">
|
||||
<button className="w-12 h-12 bg-primary rounded-full shadow-lg flex items-center justify-center text-gray-900 hover:scale-110 transition-transform">
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HowItWorks;
|
||||
69
components/Navbar.tsx
Normal file
69
components/Navbar.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
|
||||
interface NavbarProps {
|
||||
onLogoClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ onLogoClick }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<nav className="fixed w-full z-50 bg-background-dark/80 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16 items-center">
|
||||
{/* Logo */}
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={onLogoClick}
|
||||
>
|
||||
<img
|
||||
src="/assets/images/icon.png"
|
||||
alt="Nemia Logo"
|
||||
className="w-8 h-8 rounded-lg object-contain"
|
||||
/>
|
||||
<span className="font-display font-bold text-xl tracking-tight text-white">
|
||||
Nemia
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
<a href="#features" className="text-gray-300 hover:text-primary transition-colors text-sm font-medium">Features</a>
|
||||
<a href="#how-it-works" className="text-gray-300 hover:text-primary transition-colors text-sm font-medium">How it Works</a>
|
||||
<a href="#pricing" className="text-gray-300 hover:text-primary transition-colors text-sm font-medium">Pricing</a>
|
||||
<button className="bg-primary text-gray-900 px-5 py-2 rounded-full font-semibold text-sm hover:brightness-110 transition-all shadow-glow">
|
||||
Get Started
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="md:hidden flex items-center">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{isOpen && (
|
||||
<div className="md:hidden bg-surface-dark border-b border-gray-800">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
||||
<a href="#features" className="block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-800">Features</a>
|
||||
<a href="#how-it-works" className="block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-800">How it Works</a>
|
||||
<a href="#pricing" className="block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-800">Pricing</a>
|
||||
<button className="w-full mt-4 bg-primary text-gray-900 px-5 py-3 rounded-xl font-bold hover:brightness-110 transition-all">
|
||||
Get Started
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
87
components/PrivacyPolicy.tsx
Normal file
87
components/PrivacyPolicy.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
interface PrivacyPolicyProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const PrivacyPolicy: React.FC<PrivacyPolicyProps> = ({ onBack }) => {
|
||||
return (
|
||||
<div className="bg-background-dark min-h-screen text-text-light font-sans pt-32 pb-20">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-primary hover:text-emerald-400 transition-colors mb-8 group"
|
||||
>
|
||||
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</button>
|
||||
|
||||
<h1 className="text-4xl lg:text-5xl font-display font-bold mb-8 text-white">Privacy Policy</h1>
|
||||
|
||||
<div className="space-y-8 text-gray-400 leading-relaxed">
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-white mb-4">1. Introduction</h2>
|
||||
<p>
|
||||
Welcome to Nemia. We are committed to protecting your personal information and your right to privacy. If you have any questions or concerns about our policy, or our practices with regards to your personal information, please contact us.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-white mb-4">2. Information We Collect</h2>
|
||||
<p>
|
||||
We collect personal information that you provide to us such as name, email address, and study data when you register an account via Google Sign-In. We also collect data related to your flashcards, decks, and study progress to enable cloud synchronization.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
<strong>Focus Mode Data:</strong> On Android, Nemia requires Accessibility and Overlay permissions to detect foreground apps and provide blocking functionality. This data is processed locally on your device and is never uploaded to our servers.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-white mb-4">3. How We Use Your Information</h2>
|
||||
<p>
|
||||
We use the information we collect or receive to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mt-4 space-y-2">
|
||||
<li>Facilitate account creation and logon process via Google OAuth.</li>
|
||||
<li>Synchronize your study data across multiple devices.</li>
|
||||
<li>Provide and maintain the Focus Mode functionality.</li>
|
||||
<li>Send you study reminders (if enabled).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-white mb-4">4. Sharing Your Information</h2>
|
||||
<p>
|
||||
We only share information with your consent, to comply with laws, to provide you with services, to protect your rights, or to fulfill business obligations. We use Supabase for backend services and Google for authentication.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-white mb-4">5. Data Retention</h2>
|
||||
<p>
|
||||
We keep your information for as long as necessary to fulfill the purposes outlined in this privacy policy unless otherwise required by law.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-white mb-4">6. Security</h2>
|
||||
<p>
|
||||
We aim to protect your personal information through a system of organizational and technical security measures provided by our infrastructure partners (Supabase).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-white mb-4">7. Contact Us</h2>
|
||||
<p>
|
||||
If you have questions or comments about this policy, you may email us at support@nemia.app.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPolicy;
|
||||
|
||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "80:80"
|
||||
restart: always
|
||||
env_file:
|
||||
- .env.local
|
||||
|
||||
24
implementation-plan.mdc
Normal file
24
implementation-plan.mdc
Normal file
@@ -0,0 +1,24 @@
|
||||
# Implementation Plan - Dockerization
|
||||
|
||||
This plan outlines the steps for dockerizing the Nemia application for deployment on a VPS.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `.dockerignore` file. [Done]
|
||||
Added standard exclusions for Node.js projects to keep the build context small.
|
||||
2. Create `Dockerfile` with multi-stage build. [Done]
|
||||
Implemented a two-stage build using Node 20 for building and Nginx for serving.
|
||||
3. Create `nginx.conf` for optimized SPA serving. [Done]
|
||||
Configured Nginx to handle SPA routing and static asset caching.
|
||||
4. Create `docker-compose.yml` for easy deployment. [Done]
|
||||
Added a basic compose file to manage the container and environment variables.
|
||||
5. Verify build process. [Done]
|
||||
Manually reviewed Dockerfile and confirmed it matches Vite/Nginx best practices.
|
||||
6. Remove Login button and update website claims. [Done]
|
||||
Updated Navbar, Hero, Features, and HowItWorks components to accurately reflect Android-exclusive Focus Mode and AI prompt-based card generation.
|
||||
7. Add Privacy Policy. [Done]
|
||||
Created a new PrivacyPolicy component and implemented simple routing in App.tsx.
|
||||
8. Final UI Cleanup and Branding. [Done]
|
||||
Removed "free for up to 3 decks" claim, simplified footer by removing Product/Resource columns and GitHub icon, updated copyright year to 2026, and replaced all placeholder logos with assets/images/icon.png.
|
||||
9. Implement Launchlist Waitlist. [Done]
|
||||
Added email input forms in Hero and CallToAction components that submit directly to the Launchlist action URL.
|
||||
74
index.html
Normal file
74
index.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nemia - Master Anything with Focus</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Poppins:wght@500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
|
||||
<!-- Tailwind Config -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: "#00E0B8", // Vibrant teal
|
||||
secondary: "#6B7280",
|
||||
"background-dark": "#0F111A", // Very dark navy/black
|
||||
"surface-dark": "#1E202E", // Slightly lighter dark for cards
|
||||
"surface-accent": "#2D3042", // Even lighter for hover states
|
||||
"text-light": "#E5E7EB", // Light text for dark mode
|
||||
"text-muted": "#9CA3AF", // Muted text for dark mode
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
display: ['Poppins', 'sans-serif'],
|
||||
},
|
||||
borderRadius: {
|
||||
DEFAULT: "0.75rem",
|
||||
'xl': "1rem",
|
||||
'2xl': "1.5rem",
|
||||
},
|
||||
boxShadow: {
|
||||
'glow': '0 0 20px rgba(0, 224, 184, 0.15)',
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html { scroll-behavior: smooth; }
|
||||
body { background-color: #0F111A; color: #E5E7EB; }
|
||||
.mesh-gradient {
|
||||
background-color: #0F111A;
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, hsla(169, 100%, 44%, 0.1) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, hsla(250, 48%, 30%, 0.15) 0px, transparent 50%);
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@^19.2.3",
|
||||
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
|
||||
"react/": "https://esm.sh/react@^19.2.3/",
|
||||
"lucide-react": "https://esm.sh/lucide-react@^0.562.0"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
15
index.tsx
Normal file
15
index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
5
metadata.json
Normal file
5
metadata.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Nemia - Focus & Learn",
|
||||
"description": "A productivity and learning application combining app blocking with spaced repetition flashcards.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
23
nginx.conf
Normal file
23
nginx.conf
Normal file
@@ -0,0 +1,23 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location /assets/ {
|
||||
root /usr/share/nginx/html;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
1816
package-lock.json
generated
Normal file
1816
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "nemia---focus-&-learn",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"lucide-react": "^0.562.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
23
vite.config.ts
Normal file
23
vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user