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;
|
||||
}
|
||||
|
||||
.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 @@
|
||||
<div id="quotes-container"></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>
|
||||
async function loadQuotes() {
|
||||
const container = document.getElementById('quotes-container');
|
||||
@ -188,7 +499,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = quotes.map(q => {
|
||||
container.innerHTML = quotes.map((q, index) => {
|
||||
const date = new Date(q.timestamp);
|
||||
const formattedDate = date.toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
@ -198,11 +509,68 @@
|
||||
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 `
|
||||
<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;">
|
||||
<div class="quote-text">"${q.quote}"</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>
|
||||
`;
|
||||
}).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();
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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:
|
||||
- Short and concise (1-3 sentences)
|
||||
- Bold, confident and unapologetic about being gay
|
||||
- Celebrate gay culture, freedom and pride
|
||||
- Witty, sarcastic and clever
|
||||
- Support LGBTQ+ community and gay lifestyle
|
||||
- Elegant but with attitude
|
||||
- Short and punchy (1-2 sentences max)
|
||||
- Direct, crude and unapologetic
|
||||
- Sexually explicit but clever
|
||||
- Dark humor with attitude
|
||||
- Brutally honest about gay life and sex
|
||||
|
||||
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!`;
|
||||
|
||||
@ -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, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user