new ver
This commit is contained in:
parent
d8c8623580
commit
1841d8c356
30
.kiro/steering/product.md
Normal file
30
.kiro/steering/product.md
Normal file
@ -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
|
||||||
61
.kiro/steering/structure.md
Normal file
61
.kiro/steering/structure.md
Normal file
@ -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
|
||||||
48
.kiro/steering/tech.md
Normal file
48
.kiro/steering/tech.md
Normal file
@ -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
|
||||||
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
@ -120,6 +120,309 @@
|
|||||||
font-weight: 600;
|
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 {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
@ -174,6 +477,14 @@
|
|||||||
<div id="quotes-container"></div>
|
<div id="quotes-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="emoji-picker-overlay" class="emoji-picker-overlay" onclick="closeEmojiPicker(event)">
|
||||||
|
<div class="emoji-picker" onclick="event.stopPropagation()">
|
||||||
|
<div class="emoji-picker-title">Выберите реакцию</div>
|
||||||
|
<div class="emoji-grid" id="emoji-grid"></div>
|
||||||
|
<button class="emoji-picker-close" onclick="closeEmojiPicker()">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function loadQuotes() {
|
async function loadQuotes() {
|
||||||
const container = document.getElementById('quotes-container');
|
const container = document.getElementById('quotes-container');
|
||||||
@ -188,7 +499,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = quotes.map(q => {
|
container.innerHTML = quotes.map((q, index) => {
|
||||||
const date = new Date(q.timestamp);
|
const date = new Date(q.timestamp);
|
||||||
const formattedDate = date.toLocaleString('ru-RU', {
|
const formattedDate = date.toLocaleString('ru-RU', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@ -198,11 +509,68 @@
|
|||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reactions = q.reactions || {};
|
||||||
|
const reactionsHtml = Object.entries(reactions).map(([emoji, count]) =>
|
||||||
|
`<button class="reaction-btn" onclick="addReaction(${index}, '${emoji}')">
|
||||||
|
<span>${emoji}</span>
|
||||||
|
<span class="reaction-count">${count}</span>
|
||||||
|
</button>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const comments = q.comments || [];
|
||||||
|
const commentsHtml = comments.length > 0
|
||||||
|
? comments.map(c => {
|
||||||
|
const commentDate = new Date(c.timestamp);
|
||||||
|
const commentFormatted = commentDate.toLocaleString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
const commentReactions = c.reactions || {};
|
||||||
|
const commentReactionsHtml = Object.entries(commentReactions).map(([emoji, count]) =>
|
||||||
|
`<button class="comment-reaction-btn" onclick="addCommentReaction(${index}, ${c.id}, '${emoji}')">
|
||||||
|
<span>${emoji}</span>
|
||||||
|
<span class="comment-reaction-count">${count}</span>
|
||||||
|
</button>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="comment">
|
||||||
|
<div class="comment-header">
|
||||||
|
<span class="comment-author">${escapeHtml(c.author)}</span>
|
||||||
|
<span class="comment-timestamp">${commentFormatted}</span>
|
||||||
|
</div>
|
||||||
|
<div class="comment-text">${escapeHtml(c.text)}</div>
|
||||||
|
<div class="comment-reactions">
|
||||||
|
${commentReactionsHtml}
|
||||||
|
<button class="add-comment-reaction-btn" onclick="showCommentReactionPicker(${index}, ${c.id})">➕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')
|
||||||
|
: '<div class="no-comments">Пока нет комментариев. Будьте первым!</div>';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="quote-card">
|
<div class="quote-card">
|
||||||
<img src="photo_2023-10-27_20-55-45.jpg" alt="Денис Шулепов" style="width: 80px; height: 80px; border-radius: 50%; object-fit: cover; float: left; margin-right: 20px; border: 3px solid #667eea;">
|
<img src="photo_2023-10-27_20-55-45.jpg" alt="Денис Шулепов" style="width: 80px; height: 80px; border-radius: 50%; object-fit: cover; float: left; margin-right: 20px; border: 3px solid #667eea;">
|
||||||
<div class="quote-text">"${q.quote}"</div>
|
<div class="quote-text">"${q.quote}"</div>
|
||||||
<div class="quote-timestamp">📅 ${formattedDate}</div>
|
<div class="quote-timestamp">📅 ${formattedDate}</div>
|
||||||
|
<div class="reactions-container">
|
||||||
|
${reactionsHtml}
|
||||||
|
<button class="add-reaction-btn" onclick="showReactionPicker(${index})">➕</button>
|
||||||
|
</div>
|
||||||
|
<div class="comments-section">
|
||||||
|
<div class="comments-title">💬 Комментарии (${comments.length})</div>
|
||||||
|
${commentsHtml}
|
||||||
|
<form class="comment-form" onsubmit="addComment(event, ${index})">
|
||||||
|
<input type="text" class="comment-input" placeholder="Ваше имя" name="author" required maxlength="50">
|
||||||
|
<textarea class="comment-input comment-textarea" placeholder="Напишите комментарий..." name="text" required maxlength="500"></textarea>
|
||||||
|
<button type="submit" class="comment-submit">Отправить</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@ -234,6 +602,141 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addReaction(index, emoji) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/quotes/${index}/react`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ emoji })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
loadQuotes();
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to add reaction');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding reaction:', error);
|
||||||
|
alert('❌ Ошибка добавления реакции');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentQuoteIndex = null;
|
||||||
|
let currentCommentId = null;
|
||||||
|
let pickerMode = 'quote'; // 'quote' or 'comment'
|
||||||
|
|
||||||
|
function showReactionPicker(index) {
|
||||||
|
currentQuoteIndex = index;
|
||||||
|
currentCommentId = null;
|
||||||
|
pickerMode = 'quote';
|
||||||
|
const emojis = ['❤️', '😍', '🔥', '👏', '🌈', '💅', '✨', '💖', '😂', '👑', '🥰', '💋', '🎉', '⭐', '💜'];
|
||||||
|
|
||||||
|
const emojiGrid = document.getElementById('emoji-grid');
|
||||||
|
emojiGrid.innerHTML = emojis.map(emoji =>
|
||||||
|
`<button class="emoji-option" onclick="selectEmoji('${emoji}')">${emoji}</button>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
document.getElementById('emoji-picker-overlay').classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCommentReactionPicker(quoteIndex, commentId) {
|
||||||
|
currentQuoteIndex = quoteIndex;
|
||||||
|
currentCommentId = commentId;
|
||||||
|
pickerMode = 'comment';
|
||||||
|
const emojis = ['❤️', '😍', '🔥', '👏', '🌈', '💅', '✨', '💖', '😂', '👑', '🥰', '💋', '🎉', '⭐', '💜'];
|
||||||
|
|
||||||
|
const emojiGrid = document.getElementById('emoji-grid');
|
||||||
|
emojiGrid.innerHTML = emojis.map(emoji =>
|
||||||
|
`<button class="emoji-option" onclick="selectEmoji('${emoji}')">${emoji}</button>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
document.getElementById('emoji-picker-overlay').classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEmojiPicker(event) {
|
||||||
|
if (event && event.target.id !== 'emoji-picker-overlay') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('emoji-picker-overlay').classList.remove('show');
|
||||||
|
currentQuoteIndex = null;
|
||||||
|
currentCommentId = null;
|
||||||
|
pickerMode = 'quote';
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectEmoji(emoji) {
|
||||||
|
if (pickerMode === 'quote' && currentQuoteIndex !== null) {
|
||||||
|
addReaction(currentQuoteIndex, emoji);
|
||||||
|
} else if (pickerMode === 'comment' && currentQuoteIndex !== null && currentCommentId !== null) {
|
||||||
|
addCommentReaction(currentQuoteIndex, currentCommentId, emoji);
|
||||||
|
}
|
||||||
|
document.getElementById('emoji-picker-overlay').classList.remove('show');
|
||||||
|
currentQuoteIndex = null;
|
||||||
|
currentCommentId = null;
|
||||||
|
pickerMode = 'quote';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addComment(event, index) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const form = event.target;
|
||||||
|
const author = form.author.value.trim();
|
||||||
|
const text = form.text.value.trim();
|
||||||
|
|
||||||
|
if (!author || !text) {
|
||||||
|
alert('Пожалуйста, заполните все поля');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/quotes/${index}/comment`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ author, text })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
form.reset();
|
||||||
|
loadQuotes();
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to add comment');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding comment:', error);
|
||||||
|
alert('❌ Ошибка добавления комментария');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addCommentReaction(quoteIndex, commentId, emoji) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/quotes/${quoteIndex}/comment/${commentId}/react`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ emoji })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
loadQuotes();
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to add comment reaction');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding comment reaction:', error);
|
||||||
|
alert('❌ Ошибка добавления реакции на комментарий');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadQuotes();
|
loadQuotes();
|
||||||
|
|
||||||
setInterval(loadQuotes, 60000);
|
setInterval(loadQuotes, 60000);
|
||||||
|
|||||||
163
src/index.js
163
src/index.js
@ -26,18 +26,19 @@ if (!fs.existsSync(QUOTES_DIR)) {
|
|||||||
fs.mkdirSync(QUOTES_DIR, { recursive: true });
|
fs.mkdirSync(QUOTES_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROMPT = `Generate a gay pride quote in Russian on behalf of Denis Shulepov.
|
const PROMPT = `Generate an edgy, provocative gay quote in Russian on behalf of Denis Shulepov in the style of Jason Statham's tough-guy quotes.
|
||||||
The quote should be:
|
The quote should be:
|
||||||
- Short and concise (1-3 sentences)
|
- Short and punchy (1-2 sentences max)
|
||||||
- Bold, confident and unapologetic about being gay
|
- Direct, crude and unapologetic
|
||||||
- Celebrate gay culture, freedom and pride
|
- Sexually explicit but clever
|
||||||
- Witty, sarcastic and clever
|
- Dark humor with attitude
|
||||||
- Support LGBTQ+ community and gay lifestyle
|
- Brutally honest about gay life and sex
|
||||||
- Elegant but with attitude
|
|
||||||
|
|
||||||
Topics: gay pride, gay culture, fashion, style, relationships, love, freedom, celebrating being gay, self-expression, beauty.
|
Style examples:
|
||||||
|
- "Каждый не отсосанный мной хуй оставляет шрамы на сердце" - Денис Шулепов ©
|
||||||
|
- "Лучше три сантиметра сзади, чем 20 спереди"
|
||||||
|
|
||||||
Use gay slang, humor and vivid imagery. Be bold and confident. The quote should celebrate gay culture and pride with style and elegance. Never use crude street language or insults.
|
Use explicit sexual language, dark humor, and provocative statements. Be crude, direct and shocking. No elegance, no political correctness - just raw, edgy gay humor.
|
||||||
|
|
||||||
Answer only with the text of the quote, without additional explanations. Answer in Russian!`;
|
Answer only with the text of the quote, without additional explanations. Answer in Russian!`;
|
||||||
|
|
||||||
@ -122,6 +123,150 @@ app.post('/api/generate', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/quotes/:index/react', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { index } = req.params;
|
||||||
|
const { emoji } = req.body;
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
return res.status(400).json({ error: 'Emoji is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotesFile = path.join(QUOTES_DIR, 'quotes.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(quotesFile)) {
|
||||||
|
return res.status(404).json({ error: 'Quotes not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(quotesFile, 'utf8');
|
||||||
|
const quotes = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
// Конвертируем индекс из reversed массива в оригинальный
|
||||||
|
const actualIndex = quotes.length - 1 - parseInt(index);
|
||||||
|
|
||||||
|
if (actualIndex < 0 || actualIndex >= quotes.length) {
|
||||||
|
return res.status(404).json({ error: 'Quote not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!quotes[actualIndex].reactions) {
|
||||||
|
quotes[actualIndex].reactions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!quotes[actualIndex].reactions[emoji]) {
|
||||||
|
quotes[actualIndex].reactions[emoji] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
quotes[actualIndex].reactions[emoji]++;
|
||||||
|
|
||||||
|
fs.writeFileSync(quotesFile, JSON.stringify(quotes, null, 2));
|
||||||
|
|
||||||
|
res.json({ success: true, reactions: quotes[actualIndex].reactions });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding reaction:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to add reaction' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/quotes/:index/comment', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { index } = req.params;
|
||||||
|
const { author, text } = req.body;
|
||||||
|
|
||||||
|
if (!author || !text) {
|
||||||
|
return res.status(400).json({ error: 'Author and text are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotesFile = path.join(QUOTES_DIR, 'quotes.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(quotesFile)) {
|
||||||
|
return res.status(404).json({ error: 'Quotes not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(quotesFile, 'utf8');
|
||||||
|
const quotes = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
const actualIndex = quotes.length - 1 - parseInt(index);
|
||||||
|
|
||||||
|
if (actualIndex < 0 || actualIndex >= quotes.length) {
|
||||||
|
return res.status(404).json({ error: 'Quote not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!quotes[actualIndex].comments) {
|
||||||
|
quotes[actualIndex].comments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const comment = {
|
||||||
|
id: Date.now(),
|
||||||
|
author: author.trim(),
|
||||||
|
text: text.trim(),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
reactions: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
quotes[actualIndex].comments.push(comment);
|
||||||
|
|
||||||
|
fs.writeFileSync(quotesFile, JSON.stringify(quotes, null, 2));
|
||||||
|
|
||||||
|
res.json({ success: true, comment });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding comment:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to add comment' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/quotes/:index/comment/:commentId/react', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { index, commentId } = req.params;
|
||||||
|
const { emoji } = req.body;
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
return res.status(400).json({ error: 'Emoji is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotesFile = path.join(QUOTES_DIR, 'quotes.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(quotesFile)) {
|
||||||
|
return res.status(404).json({ error: 'Quotes not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(quotesFile, 'utf8');
|
||||||
|
const quotes = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
const actualIndex = quotes.length - 1 - parseInt(index);
|
||||||
|
|
||||||
|
if (actualIndex < 0 || actualIndex >= quotes.length) {
|
||||||
|
return res.status(404).json({ error: 'Quote not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!quotes[actualIndex].comments) {
|
||||||
|
return res.status(404).json({ error: 'No comments found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const comment = quotes[actualIndex].comments.find(c => c.id === parseInt(commentId));
|
||||||
|
|
||||||
|
if (!comment) {
|
||||||
|
return res.status(404).json({ error: 'Comment not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!comment.reactions) {
|
||||||
|
comment.reactions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!comment.reactions[emoji]) {
|
||||||
|
comment.reactions[emoji] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
comment.reactions[emoji]++;
|
||||||
|
|
||||||
|
fs.writeFileSync(quotesFile, JSON.stringify(quotes, null, 2));
|
||||||
|
|
||||||
|
res.json({ success: true, reactions: comment.reactions });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding comment reaction:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to add comment reaction' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running on http://localhost:${PORT}`);
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user