non-building checkpoint 1

This commit is contained in:
2026-01-03 11:18:56 -07:00
parent 9c64cb0c2f
commit 81ea22eae9
37 changed files with 4804 additions and 275 deletions

34
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
CMD ["nginx", "-g", "daemon off;"]

22
frontend/astro.config.mjs Normal file
View File

@@ -0,0 +1,22 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
integrations: [react()],
server: {
port: 3000,
host: true
},
vite: {
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
}
}
}
}
});

49
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,49 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Handle SPA routing
location / {
try_files $uri $uri/ /index.html;
}
# API proxy for development (in production, this would be handled separately)
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Error pages
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

21
frontend/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "daily-journal-prompt-frontend",
"type": "module",
"version": "1.0.0",
"description": "Frontend for Daily Journal Prompt Generator",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^4.0.0"
},
"devDependencies": {
"@astrojs/react": "^3.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}

View File

@@ -0,0 +1,174 @@
import React, { useState, useEffect } from 'react';
const PromptDisplay = () => {
const [prompts, setPrompts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedPrompt, setSelectedPrompt] = useState(null);
// Mock data for demonstration
const mockPrompts = [
"Write about a time when you felt completely at peace with yourself and the world around you. What were the circumstances that led to this feeling, and how did it change your perspective on life?",
"Imagine you could have a conversation with your future self 10 years from now. What questions would you ask, and what advice do you think your future self would give you?",
"Describe a place from your childhood that holds special meaning to you. What made this place so significant, and how does remembering it make you feel now?",
"Write about a skill or hobby you've always wanted to learn but never had the chance to pursue. What has held you back, and what would be the first step to starting?",
"Reflect on a mistake you made that ultimately led to personal growth. What did you learn from the experience, and how has it shaped who you are today?",
"Imagine you wake up tomorrow with the ability to understand and speak every language in the world. How would this change your life, and what would you do with this newfound ability?"
];
useEffect(() => {
// Simulate API call
setTimeout(() => {
setPrompts(mockPrompts);
setLoading(false);
}, 1000);
}, []);
const handleSelectPrompt = (index) => {
setSelectedPrompt(index);
};
const handleDrawPrompts = async () => {
setLoading(true);
setError(null);
try {
// TODO: Replace with actual API call
// const response = await fetch('/api/v1/prompts/draw');
// const data = await response.json();
// setPrompts(data.prompts);
// For now, use mock data
setTimeout(() => {
setPrompts(mockPrompts);
setSelectedPrompt(null);
setLoading(false);
}, 1000);
} catch (err) {
setError('Failed to draw prompts. Please try again.');
setLoading(false);
}
};
const handleAddToHistory = async () => {
if (selectedPrompt === null) {
setError('Please select a prompt first');
return;
}
try {
// TODO: Replace with actual API call
// await fetch(`/api/v1/prompts/select/${selectedPrompt}`, { method: 'POST' });
// For now, just show success message
alert(`Prompt ${selectedPrompt + 1} added to history!`);
setSelectedPrompt(null);
} catch (err) {
setError('Failed to add prompt to history');
}
};
if (loading) {
return (
<div className="text-center p-8">
<div className="spinner mx-auto"></div>
<p className="mt-4">Loading prompts...</p>
</div>
);
}
if (error) {
return (
<div className="alert alert-error">
<i className="fas fa-exclamation-circle mr-2"></i>
{error}
</div>
);
}
return (
<div>
{prompts.length === 0 ? (
<div className="text-center p-8">
<i className="fas fa-inbox fa-3x mb-4" style={{ color: 'var(--gray-color)' }}></i>
<h3>No Prompts Available</h3>
<p className="mb-4">The prompt pool is empty. Please fill the pool to get started.</p>
<button className="btn btn-primary" onClick={handleDrawPrompts}>
<i className="fas fa-plus"></i> Fill Pool First
</button>
</div>
) : (
<>
<div className="grid grid-cols-1 gap-4">
{prompts.map((prompt, index) => (
<div
key={index}
className={`prompt-card cursor-pointer ${selectedPrompt === index ? 'selected' : ''}`}
onClick={() => handleSelectPrompt(index)}
>
<div className="flex items-start gap-3">
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${selectedPrompt === index ? 'bg-green-100 text-green-600' : 'bg-blue-100 text-blue-600'}`}>
{selectedPrompt === index ? (
<i className="fas fa-check"></i>
) : (
<span>{index + 1}</span>
)}
</div>
<div className="flex-grow">
<p className="prompt-text">{prompt}</p>
<div className="prompt-meta">
<span>
<i className="fas fa-ruler-combined mr-1"></i>
{prompt.length} characters
</span>
<span>
{selectedPrompt === index ? (
<span className="text-green-600">
<i className="fas fa-check-circle mr-1"></i>
Selected
</span>
) : (
<span className="text-gray-500">
Click to select
</span>
)}
</span>
</div>
</div>
</div>
</div>
))}
</div>
<div className="flex justify-between items-center mt-6">
<div>
{selectedPrompt !== null && (
<button className="btn btn-success" onClick={handleAddToHistory}>
<i className="fas fa-history"></i> Add Prompt #{selectedPrompt + 1} to History
</button>
)}
</div>
<div className="flex gap-2">
<button className="btn btn-secondary" onClick={handleDrawPrompts}>
<i className="fas fa-redo"></i> Draw New Set
</button>
<button className="btn btn-primary" onClick={handleDrawPrompts}>
<i className="fas fa-dice"></i> Draw 6 More
</button>
</div>
</div>
<div className="mt-4 text-sm text-gray-600">
<p>
<i className="fas fa-info-circle mr-1"></i>
Select a prompt by clicking on it, then add it to your history. The AI will use your history to generate more relevant prompts in the future.
</p>
</div>
</>
)}
</div>
);
};
export default PromptDisplay;

View File

@@ -0,0 +1,211 @@
import React, { useState, useEffect } from 'react';
const StatsDashboard = () => {
const [stats, setStats] = useState({
pool: {
total: 0,
target: 20,
sessions: 0,
needsRefill: true
},
history: {
total: 0,
capacity: 60,
available: 60,
isFull: false
}
});
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate API calls
const fetchStats = async () => {
try {
// TODO: Replace with actual API calls
// const poolResponse = await fetch('/api/v1/prompts/stats');
// const historyResponse = await fetch('/api/v1/prompts/history/stats');
// const poolData = await poolResponse.json();
// const historyData = await historyResponse.json();
// Mock data for demonstration
setTimeout(() => {
setStats({
pool: {
total: 15,
target: 20,
sessions: Math.floor(15 / 6),
needsRefill: 15 < 20
},
history: {
total: 8,
capacity: 60,
available: 52,
isFull: false
}
});
setLoading(false);
}, 800);
} catch (error) {
console.error('Error fetching stats:', error);
setLoading(false);
}
};
fetchStats();
}, []);
const handleFillPool = async () => {
try {
// TODO: Replace with actual API call
// await fetch('/api/v1/prompts/fill-pool', { method: 'POST' });
// For now, update local state
setStats(prev => ({
...prev,
pool: {
...prev.pool,
total: prev.pool.target,
sessions: Math.floor(prev.pool.target / 6),
needsRefill: false
}
}));
alert('Prompt pool filled successfully!');
} catch (error) {
alert('Failed to fill prompt pool');
}
};
if (loading) {
return (
<div className="text-center p-4">
<div className="spinner mx-auto"></div>
<p className="mt-2 text-sm">Loading stats...</p>
</div>
);
}
return (
<div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="stats-card">
<div className="p-3">
<i className="fas fa-database fa-2x mb-2" style={{ color: 'var(--primary-color)' }}></i>
<div className="stats-value">{stats.pool.total}</div>
<div className="stats-label">Prompts in Pool</div>
<div className="mt-2 text-sm">
Target: {stats.pool.target}
</div>
</div>
</div>
<div className="stats-card">
<div className="p-3">
<i className="fas fa-history fa-2x mb-2" style={{ color: 'var(--secondary-color)' }}></i>
<div className="stats-value">{stats.history.total}</div>
<div className="stats-label">History Items</div>
<div className="mt-2 text-sm">
Capacity: {stats.history.capacity}
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<div className="flex justify-between items-center mb-1">
<span className="text-sm font-medium">Prompt Pool</span>
<span className="text-sm">{stats.pool.total}/{stats.pool.target}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${(stats.pool.total / stats.pool.target) * 100}%` }}
></div>
</div>
<div className="text-xs text-gray-600 mt-1">
{stats.pool.needsRefill ? (
<span className="text-orange-600">
<i className="fas fa-exclamation-triangle mr-1"></i>
Needs refill ({stats.pool.target - stats.pool.total} prompts needed)
</span>
) : (
<span className="text-green-600">
<i className="fas fa-check-circle mr-1"></i>
Pool is full
</span>
)}
</div>
</div>
<div>
<div className="flex justify-between items-center mb-1">
<span className="text-sm font-medium">Prompt History</span>
<span className="text-sm">{stats.history.total}/{stats.history.capacity}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-purple-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${(stats.history.total / stats.history.capacity) * 100}%` }}
></div>
</div>
<div className="text-xs text-gray-600 mt-1">
{stats.history.available} slots available
</div>
</div>
</div>
<div className="mt-6">
<h4 className="font-medium mb-3">Quick Insights</h4>
<ul className="space-y-2 text-sm">
<li className="flex items-start">
<i className="fas fa-calendar-day text-blue-600 mt-1 mr-2"></i>
<span>
<strong>{stats.pool.sessions} sessions</strong> available in pool
</span>
</li>
<li className="flex items-start">
<i className="fas fa-bolt text-yellow-600 mt-1 mr-2"></i>
<span>
{stats.pool.needsRefill ? (
<span className="text-orange-600">Pool needs refilling</span>
) : (
<span className="text-green-600">Pool is ready for use</span>
)}
</span>
</li>
<li className="flex items-start">
<i className="fas fa-brain text-purple-600 mt-1 mr-2"></i>
<span>
AI has learned from <strong>{stats.history.total} prompts</strong> in history
</span>
</li>
<li className="flex items-start">
<i className="fas fa-chart-line text-green-600 mt-1 mr-2"></i>
<span>
History is <strong>{Math.round((stats.history.total / stats.history.capacity) * 100)}% full</strong>
</span>
</li>
</ul>
</div>
{stats.pool.needsRefill && (
<div className="mt-6">
<button
className="btn btn-primary w-full"
onClick={handleFillPool}
>
<i className="fas fa-sync mr-2"></i>
Fill Prompt Pool ({stats.pool.target - stats.pool.total} prompts)
</button>
<p className="text-xs text-gray-600 mt-2 text-center">
This will use AI to generate new prompts and fill the pool to target capacity
</p>
</div>
)}
</div>
);
};
export default StatsDashboard;

View File

@@ -0,0 +1,138 @@
---
import '../styles/global.css';
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Daily Journal Prompt Generator</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
</head>
<body>
<header>
<nav>
<div class="logo">
<i class="fas fa-book-open"></i>
<h1>Daily Journal Prompt Generator</h1>
</div>
<div class="nav-links">
<a href="/"><i class="fas fa-home"></i> Home</a>
<a href="/history"><i class="fas fa-history"></i> History</a>
<a href="/stats"><i class="fas fa-chart-bar"></i> Stats</a>
</div>
</nav>
</header>
<main>
<slot />
</main>
<footer>
<p>Daily Journal Prompt Generator &copy; 2024</p>
<p>Powered by AI creativity</p>
</footer>
</body>
</html>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
.logo {
display: flex;
align-items: center;
gap: 1rem;
}
.logo i {
font-size: 2rem;
}
.logo h1 {
font-size: 1.5rem;
font-weight: 600;
}
.nav-links {
display: flex;
gap: 2rem;
}
.nav-links a {
color: white;
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav-links a:hover {
background-color: rgba(255, 255, 255, 0.1);
}
main {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
}
footer {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
padding: 2rem;
margin-top: 3rem;
}
footer p {
margin: 0.5rem 0;
}
@media (max-width: 768px) {
nav {
flex-direction: column;
gap: 1rem;
}
.nav-links {
width: 100%;
justify-content: center;
}
main {
padding: 0 1rem;
}
}
</style>

View File

@@ -0,0 +1,98 @@
---
import Layout from '../layouts/Layout.astro';
import PromptDisplay from '../components/PromptDisplay.jsx';
import StatsDashboard from '../components/StatsDashboard.jsx';
---
<Layout>
<div class="container">
<div class="text-center mb-4">
<h1><i class="fas fa-magic"></i> Welcome to Daily Journal Prompt Generator</h1>
<p class="mt-2">Get inspired with AI-generated writing prompts for your daily journal practice</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="lg:col-span-2">
<div class="card">
<div class="card-header">
<h2><i class="fas fa-scroll"></i> Today's Prompts</h2>
<div class="flex gap-2">
<button class="btn btn-primary">
<i class="fas fa-redo"></i> Draw New Prompts
</button>
<button class="btn btn-secondary">
<i class="fas fa-plus"></i> Fill Pool
</button>
</div>
</div>
<PromptDisplay client:load />
</div>
</div>
<div>
<div class="card">
<div class="card-header">
<h2><i class="fas fa-chart-bar"></i> Quick Stats</h2>
</div>
<StatsDashboard client:load />
</div>
<div class="card mt-4">
<div class="card-header">
<h2><i class="fas fa-lightbulb"></i> Quick Actions</h2>
</div>
<div class="flex flex-col gap-2">
<button class="btn btn-primary">
<i class="fas fa-dice"></i> Draw 6 Prompts
</button>
<button class="btn btn-secondary">
<i class="fas fa-sync"></i> Refill Pool
</button>
<button class="btn btn-success">
<i class="fas fa-palette"></i> Generate Themes
</button>
<button class="btn btn-warning">
<i class="fas fa-history"></i> View History
</button>
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h2><i class="fas fa-info-circle"></i> How It Works</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center">
<div class="p-4">
<i class="fas fa-robot fa-3x mb-3" style="color: var(--primary-color);"></i>
<h3>AI-Powered</h3>
<p>Prompts are generated using advanced AI models trained on creative writing</p>
</div>
</div>
<div class="text-center">
<div class="p-4">
<i class="fas fa-brain fa-3x mb-3" style="color: var(--secondary-color);"></i>
<h3>Smart History</h3>
<p>The AI learns from your previous prompts to avoid repetition and improve relevance</p>
</div>
</div>
<div class="text-center">
<div class="p-4">
<i class="fas fa-battery-full fa-3x mb-3" style="color: var(--success-color);"></i>
<h3>Prompt Pool</h3>
<p>Always have prompts ready with our caching system that maintains a pool of generated prompts</p>
</div>
</div>
</div>
</div>
</div>
</Layout>

View File

@@ -0,0 +1,362 @@
/* Global styles for Daily Journal Prompt Generator */
:root {
--primary-color: #667eea;
--secondary-color: #764ba2;
--accent-color: #f56565;
--success-color: #48bb78;
--warning-color: #ed8936;
--info-color: #4299e1;
--light-color: #f7fafc;
--dark-color: #2d3748;
--gray-color: #a0aec0;
--border-radius: 8px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: var(--dark-color);
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.2;
margin-bottom: 1rem;
color: var(--dark-color);
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.5rem;
}
p {
margin-bottom: 1rem;
}
a {
color: var(--primary-color);
text-decoration: none;
transition: var(--transition);
}
a:hover {
color: var(--secondary-color);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.btn-secondary {
background-color: white;
color: var(--primary-color);
border: 2px solid var(--primary-color);
}
.btn-secondary:hover {
background-color: var(--primary-color);
color: white;
}
.btn-success {
background-color: var(--success-color);
color: white;
}
.btn-warning {
background-color: var(--warning-color);
color: white;
}
.btn-danger {
background-color: var(--accent-color);
color: white;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
/* Cards */
.card {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 1.5rem;
margin-bottom: 1.5rem;
transition: var(--transition);
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--light-color);
}
/* Forms */
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--dark-color);
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 2px solid var(--gray-color);
border-radius: var(--border-radius);
font-size: 1rem;
transition: var(--transition);
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-control.error {
border-color: var(--accent-color);
}
.form-error {
color: var(--accent-color);
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Alerts */
.alert {
padding: 1rem;
border-radius: var(--border-radius);
margin-bottom: 1rem;
border-left: 4px solid;
}
.alert-success {
background-color: rgba(72, 187, 120, 0.1);
border-left-color: var(--success-color);
color: #22543d;
}
.alert-warning {
background-color: rgba(237, 137, 54, 0.1);
border-left-color: var(--warning-color);
color: #744210;
}
.alert-error {
background-color: rgba(245, 101, 101, 0.1);
border-left-color: var(--accent-color);
color: #742a2a;
}
.alert-info {
background-color: rgba(66, 153, 225, 0.1);
border-left-color: var(--info-color);
color: #2a4365;
}
/* Loading spinner */
.spinner {
display: inline-block;
width: 2rem;
height: 2rem;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Utility classes */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.text-center {
text-align: center;
}
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mt-4 { margin-top: 2rem; }
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
.mb-3 { margin-bottom: 1.5rem; }
.mb-4 { margin-bottom: 2rem; }
.p-1 { padding: 0.5rem; }
.p-2 { padding: 1rem; }
.p-3 { padding: 1.5rem; }
.p-4 { padding: 2rem; }
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.gap-1 { gap: 0.5rem; }
.gap-2 { gap: 1rem; }
.gap-3 { gap: 1.5rem; }
.gap-4 { gap: 2rem; }
.grid {
display: grid;
gap: 1.5rem;
}
.grid-cols-1 { grid-template-columns: 1fr; }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 768px) {
.grid-cols-2,
.grid-cols-3,
.grid-cols-4 {
grid-template-columns: 1fr;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
.btn {
padding: 0.5rem 1rem;
}
}
/* Prompt card specific styles */
.prompt-card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-left: 4px solid var(--primary-color);
}
.prompt-card.selected {
border-left-color: var(--success-color);
background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%);
}
.prompt-text {
font-size: 1.1rem;
line-height: 1.8;
color: var(--dark-color);
}
.prompt-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--light-color);
font-size: 0.875rem;
color: var(--gray-color);
}
/* Stats cards */
.stats-card {
text-align: center;
}
.stats-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary-color);
margin: 0.5rem 0;
}
.stats-label {
font-size: 0.875rem;
color: var(--gray-color);
text-transform: uppercase;
letter-spacing: 0.05em;
}