diff --git a/.kiro/steering/product.md b/.kiro/steering/product.md new file mode 100644 index 0000000..fed09e6 --- /dev/null +++ b/.kiro/steering/product.md @@ -0,0 +1,30 @@ +--- +inclusion: always +--- + +# Product Overview + +Denis Shulepov Gay Quotes Generator - an automated quote generation and display system that creates LGBTQ+ pride quotes in Russian using AI. + +## Core Features + +- Automated hourly quote generation using OpenRouter API +- Web interface displaying quotes with profile photo +- User engagement through emoji reactions +- Quote persistence in JSON format +- REST API for quote management + +## Target Audience + +Russian-speaking LGBTQ+ community members seeking bold, confident, and celebratory content about gay pride and culture. + +## Content Guidelines + +Quotes should be: +- Short and punchy (1-2 sentences max) +- Direct, crude and sexually explicit +- Dark humor with brutal honesty +- Provocative and shocking +- In the style of tough-guy action movie quotes +- In Russian language +- Unapologetically raw and edgy diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md new file mode 100644 index 0000000..6ed826b --- /dev/null +++ b/.kiro/steering/structure.md @@ -0,0 +1,61 @@ +--- +inclusion: always +--- + +# Project Structure + +## Directory Layout + +``` +/ +├── src/ +│ └── index.js # Main application entry point +├── public/ +│ ├── index.html # Frontend web interface +│ └── photo_*.jpg # Profile photo asset +├── quotes/ +│ └── quotes.json # Generated quotes storage (gitignored) +├── .env # Environment variables (gitignored) +├── .env.example # Environment template +└── package.json # Dependencies and scripts +``` + +## Architecture + +**Backend (src/index.js)** +- Express server with CORS enabled +- Static file serving from `public/` +- Cron job for hourly quote generation +- REST API endpoints for quote operations + +**Frontend (public/index.html)** +- Single-page application +- Vanilla JavaScript (no framework) +- Inline CSS with gradient theme +- Auto-refresh every 60 seconds + +## API Endpoints + +- `GET /api/quotes` - Retrieve all quotes (reversed order) +- `POST /api/generate` - Manually trigger quote generation +- `POST /api/quotes/:index/react` - Add emoji reaction to quote +- `POST /api/quotes/:index/comment` - Add comment to quote (implemented but not used in UI) + +## Data Model + +Quote object structure: +```json +{ + "timestamp": "ISO 8601 date string", + "quote": "Quote text in Russian", + "reactions": { "emoji": count }, + "comments": [{ "id", "author", "text", "timestamp" }] +} +``` + +## Conventions + +- CommonJS module system (require/module.exports) +- Synchronous file operations for data persistence +- Error logging to console +- Process exits on missing API key diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md new file mode 100644 index 0000000..797072a --- /dev/null +++ b/.kiro/steering/tech.md @@ -0,0 +1,48 @@ +--- +inclusion: always +--- + +# Technology Stack + +## Runtime & Language + +- Node.js with CommonJS modules +- JavaScript (no TypeScript) + +## Core Dependencies + +- **express** - Web server framework +- **axios** - HTTP client for API calls +- **node-cron** - Scheduled task execution +- **dotenv** - Environment variable management +- **cors** - Cross-origin resource sharing + +## External Services + +- OpenRouter API for AI quote generation +- Configurable AI model (default: gpt-3.5-turbo) + +## Common Commands + +```bash +# Install dependencies +npm install + +# Start the server +npm start + +# Server runs on port 3000 by default (configurable via PORT env var) +``` + +## Environment Configuration + +Required environment variables in `.env`: +- `OPENROUTER_API_KEY` - API key for OpenRouter service +- `MODEL` - (optional) AI model to use, defaults to gpt-3.5-turbo +- `PORT` - (optional) Server port, defaults to 3000 + +## Data Storage + +- File-based JSON storage in `quotes/quotes.json` +- No database required +- Quotes directory auto-created on startup diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 378ffe9..7aa69d5 100644 --- a/public/index.html +++ b/public/index.html @@ -120,6 +120,309 @@ font-weight: 600; } + .reactions-container { + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #eee; + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + } + + .reaction-btn { + background: #f5f5f5; + border: 2px solid transparent; + padding: 8px 15px; + border-radius: 20px; + font-size: 1.1rem; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 5px; + } + + .reaction-btn:hover { + background: #667eea; + border-color: #667eea; + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); + } + + .reaction-btn.active { + background: #667eea; + color: white; + border-color: #667eea; + } + + .reaction-count { + font-size: 0.9rem; + font-weight: 600; + min-width: 20px; + text-align: center; + } + + .add-reaction-btn { + background: white; + border: 2px dashed #ccc; + padding: 8px 15px; + border-radius: 20px; + font-size: 1.1rem; + cursor: pointer; + transition: all 0.2s; + } + + .add-reaction-btn:hover { + border-color: #667eea; + background: #f8f9ff; + } + + .emoji-picker-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + animation: fadeIn 0.2s; + padding: 20px; + overflow-y: auto; + } + + .emoji-picker-overlay.show { + display: flex; + justify-content: center; + align-items: center; + } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + .emoji-picker { + background: white; + border-radius: 20px; + padding: 25px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + max-width: 400px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + animation: slideUp 0.3s; + } + + @keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .emoji-picker-title { + font-size: 1.3rem; + font-weight: 600; + color: #333; + margin-bottom: 20px; + text-align: center; + } + + .emoji-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 10px; + margin-bottom: 15px; + } + + .emoji-option { + background: #f5f5f5; + border: 2px solid transparent; + padding: 15px; + border-radius: 12px; + font-size: 2rem; + cursor: pointer; + transition: all 0.2s; + text-align: center; + } + + .emoji-option:hover { + background: #667eea; + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + } + + .emoji-picker-close { + width: 100%; + background: #f5f5f5; + color: #666; + padding: 12px; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + } + + .emoji-picker-close:hover { + background: #e0e0e0; + } + + .comments-section { + margin-top: 20px; + padding-top: 20px; + border-top: 2px solid #f0f0f0; + } + + .comments-title { + font-size: 1.1rem; + font-weight: 600; + color: #667eea; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 8px; + } + + .comment { + background: #f8f9ff; + border-radius: 12px; + padding: 15px; + margin-bottom: 12px; + border-left: 3px solid #667eea; + } + + .comment-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + .comment-author { + font-weight: 600; + color: #667eea; + font-size: 0.95rem; + } + + .comment-timestamp { + font-size: 0.8rem; + color: #999; + } + + .comment-text { + color: #333; + line-height: 1.5; + font-size: 0.95rem; + margin-bottom: 10px; + } + + .comment-reactions { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + margin-top: 8px; + } + + .comment-reaction-btn { + background: #fff; + border: 1px solid #e0e0e0; + padding: 4px 10px; + border-radius: 15px; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 4px; + } + + .comment-reaction-btn:hover { + background: #667eea; + border-color: #667eea; + color: white; + transform: scale(1.05); + } + + .comment-reaction-count { + font-size: 0.8rem; + font-weight: 600; + } + + .add-comment-reaction-btn { + background: white; + border: 1px dashed #ccc; + padding: 4px 10px; + border-radius: 15px; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; + } + + .add-comment-reaction-btn:hover { + border-color: #667eea; + background: #f8f9ff; + } + + .comment-form { + margin-top: 15px; + display: flex; + flex-direction: column; + gap: 10px; + } + + .comment-input { + padding: 10px 15px; + border: 2px solid #e0e0e0; + border-radius: 10px; + font-size: 0.95rem; + font-family: inherit; + transition: border-color 0.2s; + } + + .comment-input:focus { + outline: none; + border-color: #667eea; + } + + .comment-textarea { + min-height: 80px; + resize: vertical; + } + + .comment-submit { + background: #667eea; + color: white; + border: none; + padding: 10px 20px; + border-radius: 10px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + align-self: flex-end; + } + + .comment-submit:hover { + background: #5568d3; + transform: translateY(-1px); + } + + .no-comments { + color: #999; + font-style: italic; + font-size: 0.9rem; + text-align: center; + padding: 15px; + } + .loading { text-align: center; color: white; @@ -174,6 +477,14 @@
+
+
+
Выберите реакцию
+
+ +
+
+