non-building checkpoint 1
This commit is contained in:
34
frontend/Dockerfile
Normal file
34
frontend/Dockerfile
Normal 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
22
frontend/astro.config.mjs
Normal 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
49
frontend/nginx.conf
Normal 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
21
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
174
frontend/src/components/PromptDisplay.jsx
Normal file
174
frontend/src/components/PromptDisplay.jsx
Normal 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;
|
||||
|
||||
211
frontend/src/components/StatsDashboard.jsx
Normal file
211
frontend/src/components/StatsDashboard.jsx
Normal 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;
|
||||
|
||||
138
frontend/src/layouts/Layout.astro
Normal file
138
frontend/src/layouts/Layout.astro
Normal 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 © 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>
|
||||
|
||||
98
frontend/src/pages/index.astro
Normal file
98
frontend/src/pages/index.astro
Normal 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>
|
||||
|
||||
362
frontend/src/styles/global.css
Normal file
362
frontend/src/styles/global.css
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user