perf(web): embed static files and templates

This commit is contained in:
2025-01-07 19:21:53 +03:00
parent 6ed054d56a
commit 02fb86049d
49 changed files with 27 additions and 7 deletions
+9
View File
@@ -0,0 +1,9 @@
package embed
import "embed"
//go:embed templates
var TemplatesFS embed.FS
//go:embed static
var StaticFS embed.FS
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
.loader {
display: block;
margin: 0 auto;
max-width: 20%;
}
.quote-form {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 100vh;
background-color: #0008;
}
+11
View File
@@ -0,0 +1,11 @@
/* 自定义媒体查询设置根字体大小 */
@media (max-width: 640px) {
html {
font-size: 14px;
}
}
@media (width>640px) {
html {
font-size: 16px;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

+16
View File
@@ -0,0 +1,16 @@
$("#auth").on("submit", function (e) {
e.preventDefault();
$.ajax({
url: "/api/auth",
type: "POST",
data: $("#auth").serialize(),
dataType: "json",
success: function (resp) {
location.reload();
},
error: function (err) {
$("#error-message").text(err.responseJSON.error);
$("#error").removeClass("hidden");
},
});
});
File diff suppressed because one or more lines are too long
+256
View File
@@ -0,0 +1,256 @@
const PAGE_SIZE = 10;
var totalPages;
var currPage = +sessionStorage.getItem("page");
if (currPage == 0) {
currPage = 1;
}
var search = sessionStorage.getItem("search");
if (search == null) {
search = "";
}
var sorting = sessionStorage.getItem("sort");
if (sorting == null) {
sorting = "-datetime";
}
var temp_quote_id;
function renderBlockQuote(quote) {
return `
<div class="border rounded-lg p-6 hover:shadow-md transition-shadow">
<div class="flex justify-between items-start">
<div>
<p class="text-lg font-[Playfair_Display] mb-2">${escapedString(quote.text)}</p>
<p class="text-sm text-gray-800">${escapedString(quote.author)}</p>
</div>
<div class="flex gap-2">
<button class="text-gray-600 hover:text-custom" onclick="quoteEdit('${quote.id}');">
<i class="fas fa-edit"></i>
</button>
<button class="text-gray-600 hover:text-red-500" onclick="quoteDelete('${quote.id}');">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="w-full flex justify-between items-center flex-wrap gap-1 mt-2">
<p class="text-xs text-gray-400">${new Date(quote.datetime).toLocaleString()}</p>
<p class="text-xs text-gray-400">Добавил ${quote.creator.name}</p>
</div>
</div>
`;
}
function load() {
var quotesCount;
$("#input-search").val(search);
$("#input-sorting").val(sorting);
container = $("#block-quotes");
$.ajax({
async: false,
url: `/api/quotes?filter=${encodeURIComponent(search)}&sort=${encodeURIComponent(sorting)}&limit=${PAGE_SIZE}&offset=${(currPage - 1)*PAGE_SIZE}`,
type: "GET",
dataType: "json",
success: function (resp) {
quotesCount = resp.pagination.totalCount
if (resp.pagination.count == 0) {
container.html("<p style='text-align: center;'><i>Чёт нету ничего...</i></p>");
return;
}
resp.quotes.forEach((quote) => {
container.append(renderBlockQuote(quote));
});
},
error: function (err) {
$("#error-message").text(err.responseJSON.error);
$("#error").removeClass("hidden");
},
complete: function () {
$("#block-quotes-loader").addClass("hidden");
},
});
totalPages = Math.ceil(quotesCount / PAGE_SIZE);
$("#btn-page-curr").text(currPage);
if (currPage > 1) {
$("#btn-page-first").removeClass("hidden");
if (currPage > 2) {
$("#btn-page-prev").text(currPage - 1);
$("#btn-page-prev").removeClass("hidden");
if (currPage > 3) {
$("#pages-prev").removeClass("hidden");
}
}
}
if (currPage < totalPages) {
$("#btn-page-last").text(totalPages);
$("#btn-page-last").removeClass("hidden");
if (currPage < totalPages - 1) {
$("#btn-page-next").text(currPage + 1);
$("#btn-page-next").removeClass("hidden");
if (currPage < totalPages - 2) {
$("#pages-next").removeClass("hidden");
}
}
}
}
function reload() {
container = $("#block-quotes");
loader = $("#block-quotes-loader");
loader.removeClass("hidden");
container.html(loader);
$("#error").addClass("hidden");
$("#btn-page-first").addClass("hidden");
$("#pages-prev").addClass("hidden");
$("#btn-page-prev").addClass("hidden");
$("#btn-page-curr").text(1);
$("#btn-page-next").addClass("hidden");
$("#pages-next").addClass("hidden");
$("#btn-page-last").addClass("hidden");
load();
}
function quoteEdit(quote_id) {
$.ajax({
url: `/api/quotes/${quote_id}`,
type: "GET",
dataType: "json",
success: function (resp) {
temp_quote_id = quote_id;
$("#edit-quote-text").val(resp.text);
$("#edit-quote-author").val(resp.author);
$("#edit-quote-datetime").val(resp.datetime.slice(0,19));
$("body").css("overflow", "hidden");
$("#quote-editor").css("top", $(window).scrollTop());
$("#quote-editor").removeClass("hidden");
},
error: function (err) {
$("#quote-editor-error-message").text(err.responseJSON.error);
$("#quote-editor-error").removeClass("hidden");
},
});
}
function quoteDelete(quote_id) {
$.ajax({
url: `/api/quotes/${quote_id}`,
type: "DELETE",
success: function (resp) {
reload();
$("#error").addClass("hidden");
},
error: function (err) {
$("#error-message").text(err.responseJSON.error);
$("#error").removeClass("hidden");
},
});
}
$(document).on("click", "#btn-add-open", function (e) {
now = new Date;
now = new Date(now.getTime() - now.getTimezoneOffset() * 60000);
$("#new-quote-datetime").val(now.toJSON().slice(0,19));
$("body").css("overflow", "hidden");
$("#quote-creator").css("top", $(window).scrollTop());
$("#quote-creator").removeClass("hidden");
});
$(document).on("click", "#btn-add-close", function (e) {
$("#quote-creator").addClass("hidden");
$("body").css("overflow", "");
});
$(document).on("submit", "#quote-create", function (e) {
e.preventDefault();
data = formToJSON($("#quote-create"));
data.datetime = datetimeToLocalISO(data.datetime);
$.ajax({
url: "/api/quotes",
type: "POST",
contentType: "application/json",
data: JSON.stringify(data),
processData: false,
dataType: "json",
success: function (resp) {
$("#quote-creator").addClass("hidden");
$("body").css("overflow", "");
reload();
$("#new-quote-text").val("");
$("#new-quote-author").val("");
},
error: function (err) {
$("#quote-creator-error-message").text(err.responseJSON.error);
$("#quote-creator-error").removeClass("hidden");
},
});
});
$(document).on("submit", "#quote-update", function (e) {
e.preventDefault();
data = formToJSON($("#quote-update"));
data.datetime = datetimeToLocalISO(data.datetime);
$.ajax({
url: `/api/quotes/${temp_quote_id}`,
type: "PATCH",
contentType: "application/json",
data: JSON.stringify(data),
processData: false,
dataType: "json",
success: function (resp) {
$("#quote-editor").addClass("hidden");
$("body").css("overflow", "");
reload();
$("#new-quote-text").val("");
$("#new-quote-author").val("");
},
error: function (err) {
$("#quote-editor-error-message").text(err.responseJSON.error);
$("#quote-editor-error").removeClass("hidden");
},
});
});
$(document).on("click", "#btn-edit-close", function (e) {
$("body").css("overflow", "");
$("#quote-editor").addClass("hidden");
$("#quote-editor textarea,input").val("");
$("#quote-editor-error").addClass("hidden");
});
$(window).on("load", function (e) {
load();
});
$(document).on("click", "#btn-refresh", function (e) {
search = $("#input-search").val();
if (search != "") {
currPage = 1;
sessionStorage.setItem("search", currPage);
}
sorting = $("#input-sorting option:selected").val();
reload();
sessionStorage.setItem("search", search);
sessionStorage.setItem("sort", sorting);
});
$(document).on("click", "#btn-page-first", function (e) {
currPage = 1;
reload();
sessionStorage.setItem("page", currPage);
});
$(document).on("click", "#btn-page-prev", function (e) {
currPage--;
reload();
sessionStorage.setItem("page", currPage);
});
$(document).on("click", "#btn-page-next", function (e) {
currPage++;
reload();
sessionStorage.setItem("page", currPage);
});
$(document).on("click", "#btn-page-last", function (e) {
currPage = totalPages;
reload();
sessionStorage.setItem("page", currPage);
});
+53
View File
@@ -0,0 +1,53 @@
$(window).on("load", function (e) {
$.ajax({
url: "/api/auth",
type: "GET",
dataType: "json",
success: function (resp) {
$("#input-name").val(resp.name);
$("#input-login").val(resp.login);
$("#input-tgid").val(resp.telegram_id);
},
error: function (err) {
$("#error-message").text(err.responseJSON.error);
$("#error").removeClass("hidden");
},
});
});
$(document).on("click", "#btn-logout", function (e) {
$.ajax({
url: "/api/auth",
type: "DELETE",
success: function () {
location.reload();
},
error: function (err) {
$("#error-message").text(err.responseJSON.error);
$("#error").removeClass("hidden");
},
});
});
$(document).on("submit", "#user-update", function (e) {
e.preventDefault();
data = formToJSON($("#user-update"));
$.ajax({
url: "/api/auth",
type: "PATCH",
contentType: "application/json",
data: JSON.stringify(data),
processData: false,
dataType: "json",
success: function () {
$("#error").addClass("hidden");
$("#success").removeClass("hidden");
$("#input-password").val("");
},
error: function (err) {
$("#success").addClass("hidden");
$("#error-message").text(err.responseJSON.error);
$("#error").removeClass("hidden");
},
});
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+42
View File
@@ -0,0 +1,42 @@
function datetimeToLocalISO(datetime) {
options = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: "longOffset",
};
formatter = new Intl.DateTimeFormat("iso", options);
date = new Date(datetime);
parts = {}
formatter
.formatToParts(date)
.map(({ type, value }) => {
if (type === "timeZoneName") {
value = value.slice(3);
}
if (type !== "literal") {
parts[type] = value;
}
});
return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${parts.timeZoneName}`;
}
function escapedString(str) {
return str
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\n", "<br>");
}
function formToJSON(form) {
formdata = form.serializeArray();
data = {};
$(formdata).each(function (index, obj) {
data[obj.name] = obj.value;
});
return data;
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/ms-icon-70x70.png" />
<square150x150logo src="/ms-icon-150x150.png" />
<square310x310logo src="/ms-icon-310x310.png" />
<TileColor>#8B6D5C</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

@@ -0,0 +1,46 @@
{
"name": "SkazaNull",
"lang": "ru-RU",
"start_url": "/",
"scope": "/",
"display": "standalone",
"theme_color": "#8B6D5C",
"icons": [
{
"src": "/static/images/android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75"
},
{
"src": "/static/images/android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0"
},
{
"src": "/static/images/android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "/static/images/android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "/static/images/android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "/static/images/android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+75
View File
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "head" . }}
<title>Авторизация | SkazaNull</title>
</head>
<body class="min-h-screen bg-gray-50 flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-xl w-full space-y-8 bg-white p-10 rounded-lg shadow-lg">
<div class="text-center">
<i class="fas fa-quote-left text-4xl text-custom mb-4"></i>
<h2 class="mt-6 text-3xl font-bold font-[&#39;Playfair_Display&#39;] text-gray-900">Категорически приветствую!</h2>
<p class="mt-2 text-sm text-gray-600 font-[&#39;Inter&#39;]">&#34;Пацанский веб-сервис для пацанских цитат&#34;</p>
</div>
<div id="error" class="hidden mt-4 p-4 rounded-md bg-red-50 border border-red-200 animate-fade-in"
style="position: relative; transition: all 0.3s ease-in-out;">
<p class="text-sm text-red-600 font-[&#39;Inter&#39;]">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
<span class="text-sm text-red-600 font-[&#34;Inter&#34;]" id="error-message">
Упс, чёто пошло не так...
</span>
</div>
</p>
<button class="absolute top-2 right-2 text-red-400 hover:text-red-600" onclick="this.parentElement.style.display='none'">
<i class="fas fa-times"></i>
</button>
</div>
<form class="mt-8 space-y-6" id="auth" action="/api/auth" method="POST">
<div class="space-y-5">
<div>
<label for="login" class="block text-sm font-medium text-gray-700 font-[&#39;Inter&#39;]">
Электронная почта
</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-envelope text-gray-400"></i>
</div>
<input id="login" name="login" type="login" required placeholder="Логин"
class="!rounded-button appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#39;Inter&#39;]" />
</div>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 font-[&#39;Inter&#39;]">
Пароль
</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400"></i>
</div>
<input id="password" name="password" type="password" required placeholder="Пароль"
class="!rounded-button appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#39;Inter&#39;]" />
</div>
</div>
</div>
<div>
<button type="submit"
class="!rounded-button group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium text-white bg-custom hover:bg-custom/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom font-[&#39;Inter&#39;]">
Войти
</button>
</div>
</form>
<div class="text-center">
<p class="text-xs text-gray-500 font-[&#39;Inter&#39;]">
<i class="fas fa-quote-right text-custom mr-1"></i>
&copy; Masahiko AMANO (H1K0), 2025—present
<i class="fas fa-quote-left text-custom ml-1"></i>
</p>
</div>
</div>
<script src="/static/js/auth.js"></script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
{{ define "head" }}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="apple-touch-icon" sizes="57x57" href="/static/images/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/static/images/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/static/images/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/images/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/static/images/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/images/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/static/images/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/images/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
<link rel="manifest" href="/skazanull.webmanifest">
<meta name="msapplication-TileColor" content="#8B6D5C">
<meta name="msapplication-TileImage" content="/static/images/ms-icon-144x144.png">
<meta name="theme-color" content="#8B6D5C">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&amp;family=Inter:wght@400;500;600&amp;display=swap" rel="stylesheet" />
<link href="/static/css/font-awesome.6.4.0.min.css" rel="stylesheet"/>
<link href="/static/css/tailwind-custom.css" rel="stylesheet"/>
<link href="/static/css/skazanull.css" rel="stylesheet"/>
<script src="/static/js/tailwind.3.4.5.es"></script>
<script src="/static/js/tailwind-config.min.js" data-color="#000000" data-border-radius="small"></script>
<script src="/static/js/jquery-3.6.0.min.js"></script>
{{ end }}
+152
View File
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "head" . }}
<title>Цитаты | SkazaNull</title>
</head>
<body class="min-h-screen bg-gray-50 flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-4xl w-full space-y-8 bg-white p-10 rounded-lg shadow-lg">
<div class="text-center">
<i class="fas fa-quote-left text-4xl text-custom mb-4"></i>
<h2 class="mt-6 text-3xl font-bold font-[&#39;Playfair_Display&#39;] text-gray-900">Пацанские цитаты</h2>
<p class="mt-2 text-sm text-gray-600 font-[&#39;Inter&#39;]">Читайте и угорайте :)</p>
</div>
<div id="error" class="hidden mt-4 p-4 rounded-md bg-red-50 border border-red-200">
<p id="error-message" class="text-sm text-red-600 font-[&#39;Inter&#39;]"></p>
</div>
<div class="mb-6 flex justify-between items-center flex-wrap gap-4">
<div class="flex flex-wrap gap-4">
<input type="text" id="input-search" placeholder="Поиск цитат..." class="px-4 py-2 border rounded-lg" />
<select id="input-sorting" class="px-4 py-2 border rounded-lg" style="padding-right: 3.5rem;">
<option value="-datetime">По дате ↑</option>
<option value="+datetime">По дате ↓</option>
<option value="+author">По автору А</option>
<option value="-author">По автору Я-А</option>
<option value="+text">По тексту А</option>
<option value="-text">По тексту Я-А</option>
<option value="+creator.name">По цитатору А</option>
<option value="-creator.name">По цитатору Я-А</option>
<option value="random">Рандом Рандомыч</option>
</select>
<button class="text-gray-600 hover:text-blue-500" id="btn-refresh">
<i class="fas fa-refresh"></i>
</button>
</div>
<button class="bg-custom text-white px-4 py-2 rounded-lg hover:bg-custom/90" id="btn-add-open">Добавить цитату</button>
</div>
<hr>
<div id="block-quotes" class="space-y-4">
<img id="block-quotes-loader" src="/static/images/loader.gif" alt="Loading..." class="loader">
</div>
<hr>
<div class="mt-6 flex justify-center gap-2">
<button id="btn-page-first" class="hidden px-3 py-1 border rounded-lg hover:bg-gray-50">1</button>
<div id="pages-prev" class="hidden px-3 py-1 border rounded-lg hover:bg-gray-50">...</div>
<button id="btn-page-prev" class="hidden px-3 py-1 border rounded-lg hover:bg-gray-50">1</button>
<button id="btn-page-curr" class="px-3 py-1 border rounded-lg bg-custom text-white">2</button>
<button id="btn-page-next" class="hidden px-3 py-1 border rounded-lg hover:bg-gray-50">3</button>
<div id="pages-next" class="hidden px-3 py-1 border rounded-lg hover:bg-gray-50">...</div>
<button id="btn-page-last" class="hidden px-3 py-1 border rounded-lg hover:bg-gray-50">10</button>
</div>
<a href="/settings" class="block fas fa-gear text-gray-800" style="text-align: center;"> Настройки</a>
<div class="text-center mt-8">
<p class="text-xs text-gray-500 font-[&#39;Inter&#39;]">
<i class="fas fa-quote-right text-custom mr-1"></i>
&copy; Masahiko AMANO (H1K0), 2025—present
<i class="fas fa-quote-left text-custom ml-1"></i>
</p>
</div>
</div>
<div id="quote-creator" class="hidden quote-form flex flex-col items-center justify-center">
<div class="max-w-4xl w-full mt-8 p-6">
<div class="max-w-4xl w-full space-y-8 bg-white p-10 rounded-lg shadow-lg">
<h3 class="text-xl font-bold font-[&#39;Playfair_Display&#39;] text-gray-900 mb-4">Создать цитату</h3>
<div id="quote-creator-error" class="hidden mt-4 p-4 rounded-md bg-red-50 border border-red-200">
<p id="quote-creator-error-message" class="text-sm text-red-600 font-[&#39;Inter&#39;]"></p>
</div>
<form id="quote-create" class="space-y-4">
<div>
<label for="new-quote-text" class="block text-sm font-medium text-gray-700 font-[&#39;Inter&#39;]">
Цитата
</label>
<textarea id="new-quote-text" name="text" rows="3" placeholder="Сказанная цитата может быть сказана тем, кто её сказанул." required
class="!rounded-button mt-1 block w-full px-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#39;Inter&#39;]"></textarea>
</div>
<div>
<label for="new-quote-author" class="block text-sm font-medium text-gray-700 font-[&#39;Inter&#39;]">
Автор
</label>
<input type="text" id="new-quote-author" name="author" placeholder="Узбекс" required
class="!rounded-button mt-1 block w-full px-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#39;Inter&#39;]" />
</div>
<div>
<label for="new-quote-datetime" class="block text-sm font-medium text-gray-700 font-[&#39;Inter&#39;]">
Дата и время
</label>
<input type="datetime-local" id="new-quote-datetime" name="datetime" step="1"
class="!rounded-button mt-1 block w-full px-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#39;Inter&#39;]" />
</div>
<div class="flex justify-end space-x-3">
<button type="button" id="btn-add-close"
class="!rounded-button px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom font-[&#39;Inter&#39;]">
Отмена
</button>
<button type="submit"
class="!rounded-button px-4 py-2 text-sm font-medium text-white bg-custom hover:bg-custom/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom font-[&#39;Inter&#39;]">
Создать
</button>
</div>
</form>
</div>
</div>
</div>
<div id="quote-editor" class="hidden quote-form flex flex-col items-center justify-center">
<div class="max-w-4xl w-full mt-8 p-6">
<div class="max-w-4xl w-full space-y-8 bg-white p-10 rounded-lg shadow-lg">
<h3 class="text-xl font-bold font-[&#34;Playfair_Display&#34;] text-gray-900 mb-4">Редактировать цитату</h3>
<div id="quote-editor-error" class="hidden mt-4 p-4 rounded-md bg-red-50 border border-red-200">
<p id="quote-editor-error-message" class="text-sm text-red-600 font-[&#39;Inter&#39;]"></p>
</div>
<form id="quote-update" class="space-y-4">
<div>
<label for="edit-quote-text" class="block text-sm font-medium text-gray-700 font-[&#34;Inter&#34;]">
Цитата
</label>
<textarea id="edit-quote-text" name="text" rows="3" placeholder="Сказанная цитата может быть сказана тем, кто её сказанул." required
class="!rounded-button mt-1 block w-full px-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#34;Inter&#34;]"></textarea>
</div>
<div>
<label for="edit-quote-author" class="block text-sm font-medium text-gray-700 font-[&#34;Inter&#34;]">
Автор
</label>
<input type="text" id="edit-quote-author" name="author" placeholder="Узбекс" required
class="!rounded-button mt-1 block w-full px-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#34;Inter&#34;]" />
</div>
<div>
<label for="edit-quote-datetime" class="block text-sm font-medium text-gray-700 font-[&#34;Inter&#34;]">
Дата и время
</label>
<input type="datetime-local" id="edit-quote-datetime" name="datetime" step="1"
class="!rounded-button mt-1 block w-full px-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#34;Inter&#34;]" />
</div>
<div class="flex justify-end space-x-3">
<button type="button" id="btn-edit-close"
class="!rounded-button px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom font-[&#34;Inter&#34;]">
Отмена
</button>
<button type="submit"
class="!rounded-button px-4 py-2 text-sm font-medium text-white bg-custom hover:bg-custom/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom font-[&#34;Inter&#34;]">
Сохранить
</button>
</div>
</form>
</div>
</div>
</div>
<script src="/static/js/utils.js"></script>
<script src="/static/js/quotes.js"></script>
</body>
</html>
+101
View File
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "head" . }}
<title>Настройки | SkazaNull</title>
</head>
<body class="min-h-screen bg-gray-50 flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-lg w-full space-y-8 bg-white p-10 rounded-lg shadow-lg">
<div class="text-center">
<i class="fas fa-quote-left text-4xl text-custom mb-4"></i>
<h2 class="mt-6 text-3xl font-bold font-[&#39;Playfair_Display&#39;] text-gray-900">
Настройки юзверя
</h2>
<p class="mt-2 text-sm text-gray-600 font-[&#39;Inter&#39;]">
Изменяй и властвуй
</p>
</div>
<div id="error" class="hidden mt-4 p-4 rounded-md bg-red-50 border border-red-200">
<p id="error-message" class="text-sm text-red-600 font-[&#39;Inter&#39;]"></p>
</div>
<div id="success" class="hidden mt-4 p-4 rounded-md bg-green-50 border border-green-200">
<p id="success-message" class="text-sm text-green-600 font-[&#39;Inter&#39;]">Юзверь обновлен!</p>
</div>
<form id="user-update" class="mt-8 space-y-6" action="/api/auth" method="PATCH">
<div class="space-y-5">
<div><label class="block text-sm font-medium text-gray-700 font-[&#34;Inter&#34;]">Имя юзверя</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-gray-400"></i>
</div>
<input type="text" id="input-name" name="name" placeholder="Например, &quot;Центробек&quot;"
class="!rounded-button appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#34;Inter&#34;]" />
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 font-[&#34;Inter&#34;]">
Логин (то, что при авторизации используется)
</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-at text-gray-400"></i>
</div>
<input type="login" id="input-login" name="login" placeholder="Например, &quot;centrobeque&quot;"
class="!rounded-button appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#34;Inter&#34;]" />
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 font-[&#34;Inter&#34;]">
Пароль
</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400"></i>
</div>
<input type="password" id="input-password" name="password" placeholder="Например, &quot;uzbeki_sila!&quot;"
class="!rounded-button appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#34;Inter&#34;]" />
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 font-[&#34;Inter&#34;]">
Telegram ID
</label>
<div class="mt-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fab fa-telegram text-gray-400"></i>
</div>
<input type="number" id="input-tgid" name="telegram_id" placeholder="Здесь должны быть ТОЛЬКО цифры"
class="!rounded-button appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 shadow-sm placeholder-gray-400 focus:outline-none focus:ring-custom focus:border-custom sm:text-sm font-[&#34;Inter&#34;]" />
</div>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex justify-between w-full">
<button type="submit"
class="!rounded-button px-6 py-2 text-sm font-medium text-white bg-custom hover:bg-custom/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom font-[&#34;Inter&#34;]">
Сохранить изменения
</button>
<button type="button" id="btn-logout"
class="!rounded-button px-6 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 font-[&#34;Inter&#34;]">
Выйти
</button>
</div>
</div>
</form>
<hr>
<a href="/quotes" class="block fas fa-book text-gray-800" style="text-align: center;"> К цитатам</a>
<div class="text-center">
<p class="text-xs text-gray-500 font-[&#39;Inter&#39;]">
<i class="fas fa-quote-right text-custom mr-1"></i>
&copy; Masahiko AMANO (H1K0), 2025—present
<i class="fas fa-quote-left text-custom ml-1"></i>
</p>
</div>
</div>
<script src="/static/js/utils.js"></script>
<script src="/static/js/settings.js"></script>
</body>
</html>