G

Untitled

public
Guest Apr 01, 2025 Never 13
Clone
Plaintext paste1.txt 433 lines (379 loc) | 22.37 KB
1
<!DOCTYPE html>
2
<html lang="ru">
3
<head>
4
<meta charset="UTF-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6
<title>Быстрый обмен контентом</title>
7
<script src="https://cdn.tailwindcss.com"></script>
8
<script src="https://cdn.jsdelivr.net/npm/lucide-static@latest/umd/lucide.js"></script>
9
<style>
10
/* Базовые стили */
11
body {
12
font-family: 'Inter', sans-serif; /* Шрифт Inter */
13
}
14
/* Стили для плавного появления/исчезновения */
15
.fade-enter-active, .fade-leave-active {
16
transition: opacity 0.5s ease;
17
}
18
.fade-enter-from, .fade-leave-to {
19
opacity: 0;
20
}
21
/* Стиль для превью изображений */
22
.preview-img {
23
max-width: 100%;
24
max-height: 200px;
25
object-fit: cover;
26
border-radius: 0.375rem; /* rounded-md */
27
}
28
/* Стиль для сообщений */
29
.message-box {
30
position: fixed;
31
top: 1rem;
32
right: 1rem;
33
padding: 1rem;
34
border-radius: 0.5rem; /* rounded-lg */
35
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
36
z-index: 50;
37
opacity: 0;
38
transition: opacity 0.3s ease-in-out;
39
}
40
.message-box.show {
41
opacity: 1;
42
}
43
.message-box.success {
44
background-color: #d1fae5; /* green-100 */
45
color: #065f46; /* green-800 */
46
}
47
.message-box.error {
48
background-color: #fee2e2; /* red-100 */
49
color: #991b1b; /* red-800 */
50
}
51
.message-box.info {
52
background-color: #dbeafe; /* blue-100 */
53
color: #1e40af; /* blue-800 */
54
}
55
</style>
56
</head>
57
<body class="bg-gray-100 text-gray-800 p-4 md:p-8">
58
59
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
60
61
<h1 class="text-2xl md:text-3xl font-bold mb-6 text-center text-blue-600">Быстрый обмен контентом</h1>
62
63
<hr class="my-6 border-gray-300">
64
65
<div class="mb-8">
66
<h2 class="text-xl font-semibold mb-4 text-gray-700">Создать новую публикацию</h2>
67
<div class="space-y-4">
68
<div>
69
<label for="textContent" class="block text-sm font-medium text-gray-600 mb-1">Введите текст:</label>
70
<textarea id="textContent" rows="4" class="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-150 ease-in-out" placeholder="Ваш текст здесь..."></textarea>
71
</div>
72
<div>
73
<label for="fileInput" class="block text-sm font-medium text-gray-600 mb-1">Или выберите файл (до 500МБ):</label>
74
<input type="file" id="fileInput" class="w-full text-sm text-gray-500
75
file:mr-4 file:py-2 file:px-4
76
file:rounded-md file:border-0
77
file:text-sm file:font-semibold
78
file:bg-blue-100 file:text-blue-700
79
hover:file:bg-blue-200 transition duration-150 ease-in-out cursor-pointer">
80
<p class="text-xs text-gray-500 mt-1">Поддерживаются изображения (jpg, png, gif) и видео (mp4, webm).</p>
81
<div id="filePreview" class="mt-2"></div>
82
</div>
83
<button id="publishBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md transition duration-150 ease-in-out flex items-center justify-center space-x-2">
84
<i data-lucide="upload-cloud" class="w-5 h-5"></i>
85
<span>Опубликовать</span>
86
</button>
87
<div id="publishResult" class="mt-4 p-3 bg-green-100 border border-green-300 text-green-800 rounded-md hidden">
88
Публикация создана! Ваш ID: <strong id="publishId" class="font-bold select-all"></strong>
89
<button id="copyIdBtn" class="ml-2 text-blue-600 hover:text-blue-800 text-sm font-medium">Копировать ID</button>
90
</div>
91
</div>
92
</div>
93
94
<hr class="my-8 border-gray-300">
95
96
<div class="mb-8">
97
<h2 class="text-xl font-semibold mb-4 text-gray-700">Найти публикацию по ID</h2>
98
<div class="flex space-x-2">
99
<input type="text" id="searchIdInput" class="flex-grow p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-150 ease-in-out" placeholder="Введите ID публикации">
100
<button id="searchBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-md transition duration-150 ease-in-out flex items-center justify-center space-x-2">
101
<i data-lucide="search" class="w-5 h-5"></i>
102
<span>Найти</span>
103
</button>
104
</div>
105
<div id="searchResult" class="mt-4 p-4 bg-gray-50 border border-gray-200 rounded-md hidden">
106
<h3 class="font-semibold mb-2 text-gray-600">Найденный контент:</h3>
107
<div id="searchContent" class="prose max-w-none break-words"></div>
108
</div>
109
</div>
110
111
<hr class="my-8 border-gray-300">
112
113
<div>
114
<h2 class="text-xl font-semibold mb-4 text-gray-700">Последние публикации (в этой сессии)</h2>
115
<div id="recentPosts" class="space-y-3">
116
<p class="text-gray-500">Здесь будут отображаться последние созданные вами публикации...</p>
117
</div>
118
</div>
119
</div>
120
121
<div id="messageBox" class="message-box"></div>
122
123
<script>
124
// --- Элементы DOM ---
125
const textContentInput = document.getElementById('textContent');
126
const fileInput = document.getElementById('fileInput');
127
const filePreview = document.getElementById('filePreview');
128
const publishBtn = document.getElementById('publishBtn');
129
const publishResult = document.getElementById('publishResult');
130
const publishId = document.getElementById('publishId');
131
const copyIdBtn = document.getElementById('copyIdBtn');
132
133
const searchIdInput = document.getElementById('searchIdInput');
134
const searchBtn = document.getElementById('searchBtn');
135
const searchResult = document.getElementById('searchResult');
136
const searchContent = document.getElementById('searchContent');
137
138
const recentPostsContainer = document.getElementById('recentPosts');
139
const messageBox = document.getElementById('messageBox');
140
141
// --- Хранилище данных (используем Local Storage для простоты) ---
142
// В реальном приложении здесь будет взаимодействие с сервером
143
const MAX_POSTS = 10; // Макс. кол-во хранимых недавних постов
144
let posts = JSON.parse(localStorage.getItem('contentHostPosts') || '[]');
145
146
// --- Функции ---
147
148
// Генерация уникального ID (простой вариант)
149
function generateId() {
150
return Math.random().toString(36).substring(2, 10) + Date.now().toString(36).substring(4);
151
}
152
153
// Отображение сообщений
154
function showMessage(text, type = 'info', duration = 3000) {
155
messageBox.textContent = text;
156
messageBox.className = `message-box ${type} show`; // Добавляем класс show
157
158
setTimeout(() => {
159
messageBox.classList.remove('show');
160
// Можно добавить небольшую задержку перед сбросом класса типа, если нужно
161
// setTimeout(() => messageBox.className = 'message-box', 300);
162
}, duration);
163
}
164
165
// Обновление списка недавних постов
166
function updateRecentPosts() {
167
recentPostsContainer.innerHTML = ''; // Очищаем контейнер
168
if (posts.length === 0) {
169
recentPostsContainer.innerHTML = '<p class="text-gray-500">Здесь будут отображаться последние созданные вами публикации...</p>';
170
return;
171
}
172
173
// Отображаем посты в обратном порядке (новые сверху)
174
[...posts].reverse().forEach(post => {
175
const postElement = document.createElement('div');
176
postElement.className = 'p-3 bg-gray-50 border border-gray-200 rounded-md text-sm cursor-pointer hover:bg-gray-100 transition duration-150 ease-in-out';
177
postElement.dataset.id = post.id; // Сохраняем ID для клика
178
179
let contentType = '';
180
let contentPreview = '';
181
182
if (post.type === 'text') {
183
contentType = 'Текст';
184
contentPreview = `"${post.content.substring(0, 50)}${post.content.length > 50 ? '...' : ''}"`;
185
} else if (post.type === 'image') {
186
contentType = 'Изображение';
187
contentPreview = post.fileName;
188
} else if (post.type === 'video') {
189
contentType = 'Видео';
190
contentPreview = post.fileName;
191
} else {
192
contentType = 'Файл';
193
contentPreview = post.fileName;
194
}
195
196
const timeAgo = getTimeAgo(post.timestamp);
197
198
postElement.innerHTML = `
199
<span class="font-mono bg-gray-200 px-1 rounded">${post.id}</span> -
200
<span class="text-gray-600">${contentType}: ${contentPreview}</span> -
201
<span class="text-gray-400 text-xs">${timeAgo}</span>
202
`;
203
204
// Добавляем обработчик клика для поиска по ID
205
postElement.addEventListener('click', () => {
206
searchIdInput.value = post.id;
207
handleSearch(); // Вызываем функцию поиска
208
});
209
210
recentPostsContainer.appendChild(postElement);
211
});
212
}
213
214
// Функция для форматирования времени "как давно"
215
function getTimeAgo(timestamp) {
216
const now = Date.now();
217
const secondsPast = Math.floor((now - timestamp) / 1000);
218
219
if (secondsPast < 60) {
220
return `${secondsPast} сек назад`;
221
}
222
const minutesPast = Math.floor(secondsPast / 60);
223
if (minutesPast < 60) {
224
return `${minutesPast} мин назад`;
225
}
226
const hoursPast = Math.floor(minutesPast / 60);
227
if (hoursPast < 24) {
228
return `${hoursPast} ч назад`;
229
}
230
const daysPast = Math.floor(hoursPast / 24);
231
return `${daysPast} д назад`;
232
}
233
234
235
// Обработка публикации
236
function handlePublish() {
237
const text = textContentInput.value.trim();
238
const file = fileInput.files[0];
239
240
if (!text && !file) {
241
showMessage('Введите текст или выберите файл для публикации.', 'error');
242
return;
243
}
244
245
// --- Симуляция проверки размера файла ---
246
const MAX_FILE_SIZE_MB = 500;
247
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
248
if (file && file.size > MAX_FILE_SIZE_BYTES) {
249
showMessage(`Файл слишком большой. Максимальный размер: ${MAX_FILE_SIZE_MB} МБ.`, 'error');
250
fileInput.value = ''; // Сбрасываем выбор файла
251
filePreview.innerHTML = '';
252
return;
253
}
254
// --- Конец симуляции ---
255
256
const newId = generateId();
257
const timestamp = Date.now();
258
let newPost = { id: newId, timestamp: timestamp };
259
260
if (file) {
261
// Приоритет файлу, если выбраны оба
262
newPost.type = file.type.startsWith('image/') ? 'image' : (file.type.startsWith('video/') ? 'video' : 'file');
263
newPost.fileName = file.name;
264
newPost.fileType = file.type;
265
// В реальном приложении здесь была бы загрузка файла на сервер
266
// Для прототипа сохраним Data URL для изображений для превью при поиске
267
if (newPost.type === 'image') {
268
const reader = new FileReader();
269
reader.onload = function(e) {
270
newPost.content = e.target.result; // Сохраняем Data URL
271
saveAndClear(newPost);
272
}
273
reader.onerror = function() {
274
showMessage('Не удалось прочитать файл изображения.', 'error');
275
}
276
reader.readAsDataURL(file);
277
return; // Выходим, т.к. сохранение произойдет асинхронно
278
} else {
279
// Для других файлов (видео, прочее) просто сохраняем имя
280
newPost.content = `Файл: ${file.name} (тип: ${file.type || 'неизвестен'})`;
281
}
282
283
} else if (text) {
284
newPost.type = 'text';
285
newPost.content = text;
286
}
287
288
saveAndClear(newPost);
289
}
290
291
// Функция сохранения поста и очистки формы
292
function saveAndClear(newPost) {
293
// Добавляем новый пост в начало массива
294
posts.unshift(newPost);
295
296
// Ограничиваем количество хранимых постов
297
if (posts.length > MAX_POSTS) {
298
posts.pop(); // Удаляем самый старый
299
}
300
301
// Сохраняем в Local Storage
302
try {
303
localStorage.setItem('contentHostPosts', JSON.stringify(posts));
304
} catch (e) {
305
console.error("Ошибка сохранения в Local Storage:", e);
306
showMessage('Не удалось сохранить данные сессии.', 'warning');
307
}
308
309
310
// Отображаем результат
311
publishId.textContent = newPost.id;
312
publishResult.classList.remove('hidden');
313
showMessage('Публикация успешно создана!', 'success');
314
315
// Очищаем поля ввода
316
textContentInput.value = '';
317
fileInput.value = '';
318
filePreview.innerHTML = '';
319
320
// Обновляем список недавних
321
updateRecentPosts();
322
323
// Скрываем результат через некоторое время
324
setTimeout(() => {
325
publishResult.classList.add('hidden');
326
}, 10000); // Скрыть через 10 секунд
327
}
328
329
// Обработка поиска
330
function handleSearch() {
331
const idToSearch = searchIdInput.value.trim();
332
if (!idToSearch) {
333
showMessage('Введите ID для поиска.', 'info');
334
return;
335
}
336
337
const foundPost = posts.find(post => post.id === idToSearch);
338
339
if (foundPost) {
340
searchContent.innerHTML = ''; // Очищаем предыдущий результат
341
let contentHTML = '';
342
if (foundPost.type === 'text') {
343
// Экранируем HTML для безопасности перед вставкой
344
const escapedText = document.createElement('div');
345
escapedText.textContent = foundPost.content;
346
contentHTML = `<p>${escapedText.innerHTML.replace(/\n/g, '<br>')}</p>`; // Заменяем переносы строк
347
} else if (foundPost.type === 'image') {
348
// Отображаем изображение, если есть Data URL
349
if (foundPost.content && foundPost.content.startsWith('data:image')) {
350
contentHTML = `<img src="${foundPost.content}" alt="Изображение ${foundPost.fileName}" class="preview-img mx-auto">`;
351
} else {
352
contentHTML = `<p>Изображение: ${foundPost.fileName}</p><p class="text-xs text-gray-500">(Превью недоступно в этой сессии)</p>`;
353
}
354
} else if (foundPost.type === 'video') {
355
contentHTML = `<p>Видео: ${foundPost.fileName}</p><p class="text-xs text-gray-500">(Воспроизведение не поддерживается в прототипе)</p>`;
356
// В реальном приложении здесь мог бы быть <video> плеер
357
} else {
358
contentHTML = `<p>Файл: ${foundPost.fileName} (тип: ${foundPost.fileType || 'неизвестен'})</p><p class="text-xs text-gray-500">(Скачивание не поддерживается в прототипе)</p>`;
359
}
360
searchContent.innerHTML = contentHTML;
361
searchResult.classList.remove('hidden');
362
showMessage(`Найден контент для ID: ${idToSearch}`, 'success');
363
364
} else {
365
searchContent.innerHTML = '<p class="text-red-600">Публикация с таким ID не найдена в этой сессии.</p>';
366
searchResult.classList.remove('hidden'); // Показываем блок, но с сообщением об ошибке
367
showMessage(`ID "${idToSearch}" не найден.`, 'error');
368
}
369
}
370
371
// Копирование ID в буфер обмена
372
function copyIdToClipboard() {
373
const id = publishId.textContent;
374
navigator.clipboard.writeText(id).then(() => {
375
showMessage('ID скопирован в буфер обмена!', 'success');
376
}).catch(err => {
377
showMessage('Не удалось скопировать ID.', 'error');
378
console.error('Ошибка копирования: ', err);
379
});
380
}
381
382
// Обработка превью файла
383
function handleFilePreview() {
384
filePreview.innerHTML = ''; // Очищаем превью
385
const file = fileInput.files[0];
386
if (!file) return;
387
388
// --- Симуляция проверки размера файла ---
389
const MAX_FILE_SIZE_MB = 500;
390
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
391
if (file.size > MAX_FILE_SIZE_BYTES) {
392
showMessage(`Файл слишком большой. Макс. размер: ${MAX_FILE_SIZE_MB} МБ.`, 'error');
393
fileInput.value = ''; // Сбрасываем выбор файла
394
return;
395
}
396
// --- Конец симуляции ---
397
398
const previewElement = document.createElement('div');
399
previewElement.className = 'text-sm text-gray-600 p-2 border rounded-md bg-gray-50';
400
401
if (file.type.startsWith('image/')) {
402
const reader = new FileReader();
403
reader.onload = function(e) {
404
const img = document.createElement('img');
405
img.src = e.target.result;
406
img.alt = `Превью ${file.name}`;
407
img.className = 'preview-img'; // Используем класс для стилизации
408
previewElement.appendChild(img);
409
const info = document.createElement('p');
410
info.textContent = `${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`;
411
info.className = 'mt-1 text-xs';
412
previewElement.appendChild(info);
413
}
414
reader.readAsDataURL(file);
415
} else if (file.type.startsWith('video/')) {
416
previewElement.innerHTML = `
417
<div class="flex items-center space-x-2">
418
<i data-lucide="video" class="w-5 h-5 text-blue-500"></i>
419
<span>${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)</span>
420
</div>`;
421
} else {
422
previewElement.innerHTML = `
423
<div class="flex items-center space-x-2">
424
<i data-lucide="file-text" class="w-5 h-5 text-gray-500"></i>
425
<span>${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)</span>
426
</div>`;
427
}
428
filePreview.appendChild(previewElement);
429
lucide.createIcons(); // Обновляем иконки, если они добавились динамически
430
}
431
432
433
// --- Инициализация и обработчики событий ---