Добавлена поддержка PWA с конфигурацией манифеста, иконок и кэширования. Обновлены стили и структура компонентов для улучшения пользовательского опыта. Реализованы фильтры на странице заметок и улучшена логика отображения. Упрощены стили и адаптивность элементов интерфейса.
This commit is contained in:
parent
6013bd1c79
commit
f59cd87ede
272
backend/public/assets/index-CRKRzJj1.js
Normal file
272
backend/public/assets/index-CRKRzJj1.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/public/assets/index-QEK5TGz3.css
Normal file
1
backend/public/assets/index-QEK5TGz3.css
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,2 @@
|
||||
try{self["workbox:window:7.2.0"]&&_()}catch{}function E(n,r){return new Promise(function(t){var i=new MessageChannel;i.port1.onmessage=function(c){t(c.data)},n.postMessage(r,[i.port2])})}function W(n){var r=function(t,i){if(typeof t!="object"||!t)return t;var c=t[Symbol.toPrimitive];if(c!==void 0){var h=c.call(t,i);if(typeof h!="object")return h;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(n,"string");return typeof r=="symbol"?r:r+""}function k(n,r){for(var t=0;t<r.length;t++){var i=r[t];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(n,W(i.key),i)}}function P(n,r){return P=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,i){return t.__proto__=i,t},P(n,r)}function j(n,r){(r==null||r>n.length)&&(r=n.length);for(var t=0,i=new Array(r);t<r;t++)i[t]=n[t];return i}function L(n,r){var t=typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(t)return(t=t.call(n)).next.bind(t);if(Array.isArray(n)||(t=function(c,h){if(c){if(typeof c=="string")return j(c,h);var l=Object.prototype.toString.call(c).slice(8,-1);return l==="Object"&&c.constructor&&(l=c.constructor.name),l==="Map"||l==="Set"?Array.from(c):l==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(l)?j(c,h):void 0}}(n))||r){t&&(n=t);var i=0;return function(){return i>=n.length?{done:!0}:{done:!1,value:n[i++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance.
|
||||
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}try{self["workbox:core:7.2.0"]&&_()}catch{}var w=function(){var n=this;this.promise=new Promise(function(r,t){n.resolve=r,n.reject=t})};function b(n,r){var t=location.href;return new URL(n,t).href===new URL(r,t).href}var g=function(n,r){this.type=n,Object.assign(this,r)};function d(n,r,t){return t?r?r(n):n:(n&&n.then||(n=Promise.resolve(n)),r?n.then(r):n)}function O(){}var x={type:"SKIP_WAITING"};function S(n,r){return n&&n.then?n.then(O):Promise.resolve()}var U=function(n){function r(v,u){var e,o;return u===void 0&&(u={}),(e=n.call(this)||this).nn={},e.tn=0,e.rn=new w,e.en=new w,e.on=new w,e.un=0,e.an=new Set,e.cn=function(){var s=e.fn,a=s.installing;e.tn>0||!b(a.scriptURL,e.sn.toString())||performance.now()>e.un+6e4?(e.vn=a,s.removeEventListener("updatefound",e.cn)):(e.hn=a,e.an.add(a),e.rn.resolve(a)),++e.tn,a.addEventListener("statechange",e.ln)},e.ln=function(s){var a=e.fn,f=s.target,p=f.state,m=f===e.vn,y={sw:f,isExternal:m,originalEvent:s};!m&&e.mn&&(y.isUpdate=!0),e.dispatchEvent(new g(p,y)),p==="installed"?e.wn=self.setTimeout(function(){p==="installed"&&a.waiting===f&&e.dispatchEvent(new g("waiting",y))},200):p==="activating"&&(clearTimeout(e.wn),m||e.en.resolve(f))},e.yn=function(s){var a=e.hn,f=a!==navigator.serviceWorker.controller;e.dispatchEvent(new g("controlling",{isExternal:f,originalEvent:s,sw:a,isUpdate:e.mn})),f||e.on.resolve(a)},e.gn=(o=function(s){var a=s.data,f=s.ports,p=s.source;return d(e.getSW(),function(){e.an.has(p)&&e.dispatchEvent(new g("message",{data:a,originalEvent:s,ports:f,sw:p}))})},function(){for(var s=[],a=0;a<arguments.length;a++)s[a]=arguments[a];try{return Promise.resolve(o.apply(this,s))}catch(f){return Promise.reject(f)}}),e.sn=v,e.nn=u,navigator.serviceWorker.addEventListener("message",e.gn),e}var t,i;i=n,(t=r).prototype=Object.create(i.prototype),t.prototype.constructor=t,P(t,i);var c,h,l=r.prototype;return l.register=function(v){var u=(v===void 0?{}:v).immediate,e=u!==void 0&&u;try{var o=this;return d(function(s,a){var f=s();return f&&f.then?f.then(a):a(f)}(function(){if(!e&&document.readyState!=="complete")return S(new Promise(function(s){return window.addEventListener("load",s)}))},function(){return o.mn=!!navigator.serviceWorker.controller,o.dn=o.pn(),d(o.bn(),function(s){o.fn=s,o.dn&&(o.hn=o.dn,o.en.resolve(o.dn),o.on.resolve(o.dn),o.dn.addEventListener("statechange",o.ln,{once:!0}));var a=o.fn.waiting;return a&&b(a.scriptURL,o.sn.toString())&&(o.hn=a,Promise.resolve().then(function(){o.dispatchEvent(new g("waiting",{sw:a,wasWaitingBeforeRegister:!0}))}).then(function(){})),o.hn&&(o.rn.resolve(o.hn),o.an.add(o.hn)),o.fn.addEventListener("updatefound",o.cn),navigator.serviceWorker.addEventListener("controllerchange",o.yn),o.fn})}))}catch(s){return Promise.reject(s)}},l.update=function(){try{return this.fn?d(S(this.fn.update())):d()}catch(v){return Promise.reject(v)}},l.getSW=function(){return this.hn!==void 0?Promise.resolve(this.hn):this.rn.promise},l.messageSW=function(v){try{return d(this.getSW(),function(u){return E(u,v)})}catch(u){return Promise.reject(u)}},l.messageSkipWaiting=function(){this.fn&&this.fn.waiting&&E(this.fn.waiting,x)},l.pn=function(){var v=navigator.serviceWorker.controller;return v&&b(v.scriptURL,this.sn.toString())?v:void 0},l.bn=function(){try{var v=this;return d(function(u,e){try{var o=u()}catch(s){return e(s)}return o&&o.then?o.then(void 0,e):o}(function(){return d(navigator.serviceWorker.register(v.sn,v.nn),function(u){return v.un=performance.now(),u})},function(u){throw u}))}catch(u){return Promise.reject(u)}},c=r,(h=[{key:"active",get:function(){return this.en.promise}},{key:"controlling",get:function(){return this.on.promise}}])&&k(c.prototype,h),Object.defineProperty(c,"prototype",{writable:!1}),c}(function(){function n(){this.Pn=new Map}var r=n.prototype;return r.addEventListener=function(t,i){this.jn(t).add(i)},r.removeEventListener=function(t,i){this.jn(t).delete(i)},r.dispatchEvent=function(t){t.target=this;for(var i,c=L(this.jn(t.type));!(i=c()).done;)(0,i.value)(t)},r.jn=function(t){return this.Pn.has(t)||this.Pn.set(t,new Set),this.Pn.get(t)},n}());export{U as Workbox,g as WorkboxEvent,E as messageSW};
|
||||
@ -1,177 +1,167 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#667eea" />
|
||||
<title>NoteJS Backend</title>
|
||||
|
||||
<!-- Предотвращение мерцания темы -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
// Получаем сохраненную тему
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
// Получаем системные предпочтения
|
||||
const systemPrefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
|
||||
// Определяем тему: сохраненная или системная
|
||||
const theme = savedTheme || (systemPrefersDark ? "dark" : "light");
|
||||
|
||||
// Устанавливаем тему до загрузки CSS
|
||||
if (theme === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
|
||||
// Получаем и устанавливаем accentColor
|
||||
const savedAccentColor = localStorage.getItem("accentColor");
|
||||
const accentColor = savedAccentColor || "#667eea";
|
||||
|
||||
// Устанавливаем CSS переменную для accent цвета
|
||||
document.documentElement.style.setProperty("--accent-color", accentColor);
|
||||
|
||||
// Устанавливаем цвет для meta theme-color
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.setAttribute(
|
||||
"content",
|
||||
theme === "dark" ? "#1a1a1a" : accentColor
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// В случае ошибки устанавливаем светлую тему по умолчанию
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
document.documentElement.style.setProperty("--accent-color", "#667eea");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<title>NoteJS - Система заметок</title>
|
||||
|
||||
<!-- Предотвращение мерцания темы -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
// Получаем сохраненную тему
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
// Получаем системные предпочтения
|
||||
const systemPrefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
|
||||
// Определяем тему: сохраненная или системная
|
||||
const theme = savedTheme || (systemPrefersDark ? "dark" : "light");
|
||||
|
||||
// Функция для конвертации hex в RGB
|
||||
function hexToRgb(hex) {
|
||||
const cleanHex = hex.replace("#", "");
|
||||
const r = parseInt(cleanHex.substring(0, 2), 16);
|
||||
const g = parseInt(cleanHex.substring(2, 4), 16);
|
||||
const b = parseInt(cleanHex.substring(4, 6), 16);
|
||||
return `${r}, ${g}, ${b}`;
|
||||
}
|
||||
|
||||
// Получаем и устанавливаем accentColor
|
||||
const savedAccentColor = localStorage.getItem("accentColor");
|
||||
const accentColor = savedAccentColor || "#007bff";
|
||||
|
||||
// Устанавливаем тему и переменные до загрузки CSS
|
||||
if (theme === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
|
||||
// Устанавливаем CSS переменные для accent цвета
|
||||
document.documentElement.style.setProperty("--accent-color", accentColor);
|
||||
document.documentElement.style.setProperty("--accent-color-rgb", hexToRgb(accentColor));
|
||||
|
||||
// Устанавливаем цвет для meta theme-color
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.setAttribute(
|
||||
"content",
|
||||
theme === "dark" ? "#1a1a1a" : accentColor
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// В случае ошибки устанавливаем светлую тему по умолчанию
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
document.documentElement.style.setProperty("--accent-color", "#007bff");
|
||||
document.documentElement.style.setProperty("--accent-color-rgb", "0, 123, 255");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Критические стили темы для предотвращения flash эффекта -->
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
:root {
|
||||
--accent-color: #007bff;
|
||||
|
||||
/* Светлая тема (по умолчанию) */
|
||||
--bg-primary: #f5f5f5;
|
||||
--text-primary: #333333;
|
||||
--border-primary: #e0e0e0;
|
||||
--shadow-light: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Темная тема */
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #1a1a1a;
|
||||
--text-primary: #ffffff;
|
||||
--border-primary: #404040;
|
||||
--shadow-light: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Применяем стили сразу к body для предотвращения flash */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
padding: 10px 20px;
|
||||
border-radius: 25px;
|
||||
margin: 20px 0;
|
||||
font-size: 1.1em;
|
||||
border: 2px solid rgba(76, 175, 80, 0.6);
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 30px 0;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #667eea;
|
||||
padding: 15px 40px;
|
||||
border-radius: 30px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
margin: 10px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="emoji">🚀</div>
|
||||
<h1>NoteJS Backend API</h1>
|
||||
|
||||
<div class="status">✅ Сервер работает</div>
|
||||
|
||||
<div class="info">
|
||||
<p>Это Backend API сервер NoteJS React.</p>
|
||||
<p>Для доступа к приложению используйте Frontend:</p>
|
||||
</div>
|
||||
|
||||
<div class="code">
|
||||
<strong>Frontend (Development):</strong><br />
|
||||
http://localhost:5173
|
||||
</div>
|
||||
|
||||
<a href="http://localhost:5173" class="button"> Открыть приложение </a>
|
||||
|
||||
<div
|
||||
class="info"
|
||||
style="margin-top: 40px; font-size: 0.9em; opacity: 0.8"
|
||||
>
|
||||
<p><strong>Backend API:</strong> http://localhost:3001</p>
|
||||
<p>
|
||||
<strong>Endpoints:</strong> /api/auth, /api/notes, /api/user, /api/ai
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Автоматическое перенаправление на frontend через 3 секунды
|
||||
setTimeout(() => {
|
||||
if (confirm("Перейти на frontend приложение?")) {
|
||||
window.location.href = "http://localhost:5173";
|
||||
}
|
||||
}, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</style>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta
|
||||
name="description"
|
||||
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
|
||||
/>
|
||||
<meta name="theme-color" content="#007bff" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="NoteJS" />
|
||||
<meta name="apple-touch-fullscreen" content="yes" />
|
||||
<meta name="msapplication-TileColor" content="#007bff" />
|
||||
<meta name="msapplication-config" content="/browserconfig.xml" />
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
|
||||
<meta name="application-name" content="NoteJS" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/icons/icon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/icons/icon-16x16.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="114x114"
|
||||
href="/icons/icon-128x128.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="120x120"
|
||||
href="/icons/icon-128x128.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="144x144"
|
||||
href="/icons/icon-144x144.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="/icons/icon-152x152.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/icons/icon-192x192.png"
|
||||
/>
|
||||
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
|
||||
|
||||
<!-- Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<script type="module" crossorigin src="/assets/index-CRKRzJj1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-QEK5TGz3.css">
|
||||
<link rel="manifest" href="/manifest.webmanifest"></head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -13,6 +13,23 @@
|
||||
"categories": ["productivity", "utilities"],
|
||||
"prefer_related_applications": false,
|
||||
"display_override": ["window-controls-overlay", "standalone"],
|
||||
"dir": "ltr",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Новая заметка",
|
||||
"short_name": "Новая",
|
||||
"description": "Создать новую заметку",
|
||||
"url": "/notes?new=true",
|
||||
"icons": [{ "src": "/icons/icon-192x192.png", "sizes": "192x192" }]
|
||||
},
|
||||
{
|
||||
"name": "Мой профиль",
|
||||
"short_name": "Профиль",
|
||||
"description": "Открыть профиль",
|
||||
"url": "/profile",
|
||||
"icons": [{ "src": "/icons/icon-192x192.png", "sizes": "192x192" }]
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/icons/icon-512x512.png",
|
||||
|
||||
1
backend/public/manifest.webmanifest
Normal file
1
backend/public/manifest.webmanifest
Normal file
@ -0,0 +1 @@
|
||||
{"name":"NoteJS - Система заметок","short_name":"NoteJS","start_url":"/","display":"standalone","background_color":"#ffffff","lang":"en","scope":"/","description":"Современная система заметок с поддержкой Markdown, изображений, тегов и календаря","theme_color":"#007bff","orientation":"portrait-primary","icons":[{"src":"/icons/icon-72x72.png","sizes":"72x72","type":"image/png","purpose":"any"},{"src":"/icons/icon-96x96.png","sizes":"96x96","type":"image/png","purpose":"any"},{"src":"/icons/icon-128x128.png","sizes":"128x128","type":"image/png","purpose":"any"},{"src":"/icons/icon-144x144.png","sizes":"144x144","type":"image/png","purpose":"any"},{"src":"/icons/icon-152x152.png","sizes":"152x152","type":"image/png","purpose":"any"},{"src":"/icons/icon-192x192.png","sizes":"192x192","type":"image/png","purpose":"any"},{"src":"/icons/icon-384x384.png","sizes":"384x384","type":"image/png","purpose":"any"},{"src":"/icons/icon-512x512.png","sizes":"512x512","type":"image/png","purpose":"any"},{"src":"/icons/icon-192x192.png","sizes":"192x192","type":"image/png","purpose":"maskable"},{"src":"/icons/icon-512x512.png","sizes":"512x512","type":"image/png","purpose":"maskable"}]}
|
||||
@ -1,17 +1 @@
|
||||
// NoteJS Backend Service Worker
|
||||
// Минимальный SW для предотвращения ошибок
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
console.log("Backend SW installed");
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
console.log("Backend SW activated");
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
// Просто пропускаем все запросы через сеть
|
||||
event.respondWith(fetch(event.request));
|
||||
});
|
||||
if(!self.define){let e,i={};const n=(n,c)=>(n=new URL(n+".js",c).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(c,o)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let r={};const d=e=>n(e,s),a={module:{uri:s},exports:r,require:d};i[s]=Promise.all(c.map(e=>a[e]||d(e))).then(e=>(o(...e),r))}}define(["./workbox-57555046"],function(e){"use strict";self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.precacheAndRoute([{url:"assets/index-CRKRzJj1.js",revision:null},{url:"assets/index-QEK5TGz3.css",revision:null},{url:"assets/workbox-window.prod.es5-B9K5rw8f.js",revision:null},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"52c85beb0841c0c7c8ddf774370cff39"},{url:"logo.svg",revision:"11616ede8898b4c24203e331b3ec6dc3"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html"))),e.registerRoute(/^https:\/\/api\./i,new e.NetworkFirst({cacheName:"api-cache",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/i,new e.CacheFirst({cacheName:"image-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});
|
||||
|
||||
1
backend/public/workbox-57555046.js
Normal file
1
backend/public/workbox-57555046.js
Normal file
File diff suppressed because one or more lines are too long
1
dev-dist/registerSW.js
Normal file
1
dev-dist/registerSW.js
Normal file
@ -0,0 +1 @@
|
||||
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
||||
109
dev-dist/sw.js
Normal file
109
dev-dist/sw.js
Normal file
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// If the loader is already loaded, just stop.
|
||||
if (!self.define) {
|
||||
let registry = {};
|
||||
|
||||
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||
let nextDefineUri;
|
||||
|
||||
const singleRequire = (uri, parentUri) => {
|
||||
uri = new URL(uri + ".js", parentUri).href;
|
||||
return registry[uri] || (
|
||||
|
||||
new Promise(resolve => {
|
||||
if ("document" in self) {
|
||||
const script = document.createElement("script");
|
||||
script.src = uri;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
nextDefineUri = uri;
|
||||
importScripts(uri);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
let promise = registry[uri];
|
||||
if (!promise) {
|
||||
throw new Error(`Module ${uri} didn’t register its module`);
|
||||
}
|
||||
return promise;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
self.define = (depsNames, factory) => {
|
||||
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||
if (registry[uri]) {
|
||||
// Module is already loading or loaded.
|
||||
return;
|
||||
}
|
||||
let exports = {};
|
||||
const require = depUri => singleRequire(depUri, uri);
|
||||
const specialDeps = {
|
||||
module: { uri },
|
||||
exports,
|
||||
require
|
||||
};
|
||||
registry[uri] = Promise.all(depsNames.map(
|
||||
depName => specialDeps[depName] || require(depName)
|
||||
)).then(deps => {
|
||||
factory(...deps);
|
||||
return exports;
|
||||
});
|
||||
};
|
||||
}
|
||||
define(['./workbox-8cfb3eb5'], (function (workbox) { 'use strict';
|
||||
|
||||
self.addEventListener('message', event => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The precacheAndRoute() method efficiently caches and responds to
|
||||
* requests for URLs in the manifest.
|
||||
* See https://goo.gl/S9QRab
|
||||
*/
|
||||
workbox.precacheAndRoute([{
|
||||
"url": "registerSW.js",
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.h9luj2l3mv8"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
allowlist: [/^\/$/]
|
||||
}));
|
||||
workbox.registerRoute(/^https:\/\/api\./, new workbox.NetworkFirst({
|
||||
"cacheName": "api-cache",
|
||||
plugins: [new workbox.ExpirationPlugin({
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 3600
|
||||
})]
|
||||
}), 'GET');
|
||||
workbox.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/, new workbox.CacheFirst({
|
||||
"cacheName": "images-cache",
|
||||
plugins: [new workbox.ExpirationPlugin({
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 2592000
|
||||
})]
|
||||
}), 'GET');
|
||||
|
||||
}));
|
||||
4600
dev-dist/workbox-8cfb3eb5.js
Normal file
4600
dev-dist/workbox-8cfb3eb5.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,10 +9,10 @@
|
||||
"orientation": "portrait-primary",
|
||||
"scope": "/",
|
||||
"lang": "ru",
|
||||
"id": "/",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"prefer_related_applications": false,
|
||||
"display_override": ["window-controls-overlay", "standalone"],
|
||||
"dir": "ltr",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/icons/icon-512x512.png",
|
||||
|
||||
@ -8,6 +8,7 @@ import NotesPage from "./pages/NotesPage";
|
||||
import ProfilePage from "./pages/ProfilePage";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
import { NotificationStack } from "./components/common/Notification";
|
||||
import { InstallPrompt } from "./components/common/InstallPrompt";
|
||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||
import { useTheme } from "./hooks/useTheme";
|
||||
|
||||
@ -17,6 +18,7 @@ const AppContent: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<NotificationStack />
|
||||
<InstallPrompt />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<LoginPage />} />
|
||||
|
||||
155
src/components/common/InstallPrompt.tsx
Normal file
155
src/components/common/InstallPrompt.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
||||
}
|
||||
|
||||
export const InstallPrompt: React.FC = () => {
|
||||
const [deferredPrompt, setDeferredPrompt] =
|
||||
useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
||||
// Показываем промпт через небольшую задержку
|
||||
setTimeout(() => setShowPrompt(true), 2000);
|
||||
};
|
||||
|
||||
window.addEventListener("beforeinstallprompt", handler);
|
||||
|
||||
// Проверяем, не установлено ли приложение уже
|
||||
if (window.matchMedia("(display-mode: standalone)").matches) {
|
||||
setShowPrompt(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeinstallprompt", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleInstallClick = async () => {
|
||||
if (!deferredPrompt) return;
|
||||
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
|
||||
if (outcome === "accepted") {
|
||||
console.log("Пользователь принял предложение об установке");
|
||||
} else {
|
||||
console.log("Пользователь отклонил предложение об установке");
|
||||
}
|
||||
|
||||
setDeferredPrompt(null);
|
||||
setShowPrompt(false);
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
setShowPrompt(false);
|
||||
// Сохраняем в localStorage, что пользователь закрыл промпт
|
||||
localStorage.setItem("pwa-install-dismissed", "true");
|
||||
};
|
||||
|
||||
// Не показываем, если пользователь уже закрыл промпт
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("pwa-install-dismissed") === "true") {
|
||||
setShowPrompt(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!showPrompt || !deferredPrompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: "20px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
border: "1px solid var(--border-primary)",
|
||||
borderRadius: "12px",
|
||||
padding: "16px 20px",
|
||||
boxShadow: "0 4px 20px var(--shadow-light)",
|
||||
zIndex: 1000,
|
||||
maxWidth: "90%",
|
||||
width: "400px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "14px",
|
||||
color: "var(--text-primary)",
|
||||
}}
|
||||
>
|
||||
Установить NoteJS?
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--text-primary)",
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
Установите приложение для быстрого доступа
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
onClick={handleInstallClick}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "var(--accent-color)",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
}}
|
||||
>
|
||||
<Icon icon="mdi:download" width="18" height="18" />
|
||||
Установить
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
style={{
|
||||
padding: "8px",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--text-primary)",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
opacity: 0.6,
|
||||
}}
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<Icon icon="mdi:close" width="20" height="20" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,11 +2,6 @@ import React, { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useAppSelector, useAppDispatch } from "../../store/hooks";
|
||||
import {
|
||||
setSelectedDate,
|
||||
setSelectedTag,
|
||||
setSearchQuery,
|
||||
} from "../../store/slices/notesSlice";
|
||||
import { userApi } from "../../api/userApi";
|
||||
import { ThemeToggle } from "../common/ThemeToggle";
|
||||
import { setUser, setAiSettings } from "../../store/slices/profileSlice";
|
||||
@ -60,35 +55,6 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
dispatch(setSelectedDate(null));
|
||||
dispatch(setSelectedTag(null));
|
||||
dispatch(setSearchQuery(""));
|
||||
};
|
||||
|
||||
const hasFilters = !!(selectedDate || selectedTag || searchQuery);
|
||||
|
||||
// Формируем список активных фильтров
|
||||
const getActiveFilters = () => {
|
||||
const filters: string[] = [];
|
||||
|
||||
if (searchQuery) {
|
||||
filters.push(`Поиск: "${searchQuery}"`);
|
||||
}
|
||||
|
||||
if (selectedDate) {
|
||||
filters.push(`Дата: ${selectedDate}`);
|
||||
}
|
||||
|
||||
if (selectedTag) {
|
||||
filters.push(`Тег: #${selectedTag}`);
|
||||
}
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
const activeFilters = getActiveFilters();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Кнопка мобильного меню */}
|
||||
@ -103,14 +69,6 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
<span>
|
||||
<Icon icon="mdi:note-text" /> Мои заметки
|
||||
</span>
|
||||
{hasFilters && (
|
||||
<div className="filter-indicator">
|
||||
<span className="filter-indicator-text">
|
||||
Фильтр: {activeFilters.join(", ")}
|
||||
</span>{" "}
|
||||
<button onClick={handleClearFilters}>✕</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="user-info">
|
||||
{user?.avatar ? (
|
||||
|
||||
@ -27,7 +27,10 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startX, setStartX] = useState(0);
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
const [menuPosition, setMenuPosition] = useState<{
|
||||
top: number;
|
||||
left: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@ -163,7 +166,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||
/>
|
||||
</button>
|
||||
{showHeaderDropdown && menuPosition && (
|
||||
<div
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="header-dropdown-menu"
|
||||
style={{
|
||||
|
||||
@ -577,7 +577,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
|
||||
// Проверяем, есть ли текст в редакторе
|
||||
const hasText = content.trim().length > 0;
|
||||
|
||||
|
||||
const position = getCursorPosition();
|
||||
if (position && hasText) {
|
||||
setToolbarPosition({ top: position.top, left: position.left });
|
||||
@ -831,7 +831,8 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
const hasText = textarea.value.trim().length > 0;
|
||||
const hasSelection = textarea.selectionStart !== textarea.selectionEnd;
|
||||
const hasSelection =
|
||||
textarea.selectionStart !== textarea.selectionEnd;
|
||||
// Блокируем браузерное меню только если есть текст И есть выделение
|
||||
if (hasText && hasSelection) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -534,7 +534,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
|
||||
// Проверяем, есть ли текст в редакторе
|
||||
const hasText = editContent.trim().length > 0;
|
||||
|
||||
|
||||
const position = getCursorPosition();
|
||||
if (position && hasText) {
|
||||
setToolbarPosition({ top: position.top, left: position.left });
|
||||
@ -1115,7 +1115,8 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const textarea = editTextareaRef.current;
|
||||
if (textarea) {
|
||||
const hasText = textarea.value.trim().length > 0;
|
||||
const hasSelection = textarea.selectionStart !== textarea.selectionEnd;
|
||||
const hasSelection =
|
||||
textarea.selectionStart !== textarea.selectionEnd;
|
||||
// Блокируем браузерное меню только если есть текст И есть выделение
|
||||
if (hasText && hasSelection) {
|
||||
e.preventDefault();
|
||||
@ -1337,7 +1338,9 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
<>
|
||||
<div
|
||||
ref={textNoteRef}
|
||||
className={`textNote ${isLongNote && !isExpanded ? "collapsed" : ""}`}
|
||||
className={`textNote ${
|
||||
isLongNote && !isExpanded ? "collapsed" : ""
|
||||
}`}
|
||||
data-original-content={note.content}
|
||||
dangerouslySetInnerHTML={{ __html: formatContent() }}
|
||||
onClick={(e) => {
|
||||
@ -1356,7 +1359,9 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
onClick={toggleExpand}
|
||||
type="button"
|
||||
>
|
||||
<Icon icon={isExpanded ? "mdi:chevron-up" : "mdi:chevron-down"} />
|
||||
<Icon
|
||||
icon={isExpanded ? "mdi:chevron-up" : "mdi:chevron-down"}
|
||||
/>
|
||||
<span>{isExpanded ? "Скрыть" : "Раскрыть"}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@ -111,9 +111,7 @@ export const NotesList = forwardRef<NotesListRef>((props, ref) => {
|
||||
|
||||
return (
|
||||
<div className="notes-container">
|
||||
<p className="empty-message">
|
||||
{message}
|
||||
</p>
|
||||
<p className="empty-message">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ import "./styles/theme.css";
|
||||
import "./styles/style.css";
|
||||
import "./styles/style-calendar.css";
|
||||
|
||||
// Регистрация PWA (vite-plugin-pwa автоматически внедряет регистрацию через injectRegister: "auto")
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
@ -135,11 +135,6 @@ const LoginPage: React.FC = () => {
|
||||
Нет аккаунта? <Link to="/register">Зарегистрируйтесь</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<p>
|
||||
Создатель: <span>Fovway</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,12 +5,49 @@ import { MobileSidebar } from "../components/layout/MobileSidebar";
|
||||
import { NoteEditor } from "../components/notes/NoteEditor";
|
||||
import { NotesList, NotesListRef } from "../components/notes/NotesList";
|
||||
import { ImageModal } from "../components/common/ImageModal";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { useAppSelector, useAppDispatch } from "../store/hooks";
|
||||
import {
|
||||
setSelectedDate,
|
||||
setSelectedTag,
|
||||
setSearchQuery,
|
||||
} from "../store/slices/notesSlice";
|
||||
|
||||
const NotesPage: React.FC = () => {
|
||||
const allNotes = useAppSelector((state) => state.notes.allNotes);
|
||||
const notesListRef = useRef<NotesListRef>(null);
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedDate = useAppSelector((state) => state.notes.selectedDate);
|
||||
const selectedTag = useAppSelector((state) => state.notes.selectedTag);
|
||||
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
|
||||
|
||||
const hasFilters = !!(selectedDate || selectedTag || searchQuery);
|
||||
|
||||
const handleClearFilters = () => {
|
||||
dispatch(setSelectedDate(null));
|
||||
dispatch(setSelectedTag(null));
|
||||
dispatch(setSearchQuery(""));
|
||||
};
|
||||
|
||||
const getActiveFilters = () => {
|
||||
const filters: string[] = [];
|
||||
|
||||
if (searchQuery) {
|
||||
filters.push(`Поиск: "${searchQuery}"`);
|
||||
}
|
||||
|
||||
if (selectedDate) {
|
||||
filters.push(`Дата: ${selectedDate}`);
|
||||
}
|
||||
|
||||
if (selectedTag) {
|
||||
filters.push(`Тег: #${selectedTag}`);
|
||||
}
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
const activeFilters = getActiveFilters();
|
||||
|
||||
const handleNoteSave = () => {
|
||||
// Вызываем перезагрузку заметок после создания новой заметки
|
||||
@ -37,14 +74,17 @@ const NotesPage: React.FC = () => {
|
||||
<div className="center">
|
||||
<div className="container">
|
||||
<Header onToggleSidebar={handleToggleMobileSidebar} />
|
||||
{hasFilters && (
|
||||
<div className="filter-indicator">
|
||||
<span className="filter-indicator-text">
|
||||
Фильтр: {activeFilters.join(", ")}
|
||||
</span>{" "}
|
||||
<button onClick={handleClearFilters}>✕</button>
|
||||
</div>
|
||||
)}
|
||||
<NoteEditor onSave={handleNoteSave} />
|
||||
</div>
|
||||
<NotesList ref={notesListRef} />
|
||||
<div className="footer">
|
||||
<p>
|
||||
Создатель: <span>Fovway</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ImageModal />
|
||||
</>
|
||||
|
||||
@ -156,11 +156,6 @@ const RegisterPage: React.FC = () => {
|
||||
Уже есть аккаунт? <Link to="/">Войдите</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<p>
|
||||
Создатель: <span>Fovway</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -329,19 +329,22 @@ header .iconify[data-icon="mdi:account-plus"] {
|
||||
header {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.notes-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.notes-header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Стили для секции поиска в левой панели */
|
||||
@ -408,7 +411,7 @@ header {
|
||||
.filter-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
margin-top: 15px;
|
||||
padding: 4px 10px;
|
||||
background-color: rgba(var(--accent-color-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--accent-color-rgb), 0.3);
|
||||
@ -416,7 +419,8 @@ header {
|
||||
font-size: 12px;
|
||||
color: var(--accent-color);
|
||||
font-weight: 500;
|
||||
max-width: 300px;
|
||||
max-width: min(300px, 100%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-indicator-text {
|
||||
@ -454,6 +458,7 @@ header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-info span {
|
||||
@ -510,27 +515,25 @@ header {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-icon-btn:hover {
|
||||
background: var(--accent-color, #007bff);
|
||||
background-color: var(--accent-color, #007bff);
|
||||
color: white;
|
||||
transform: rotate(45deg);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.settings-icon-btn .iconify {
|
||||
font-size: 20px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
color: var(--text-secondary);
|
||||
transition: color 0.3s ease;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.settings-icon-btn:hover .iconify {
|
||||
@ -2698,10 +2701,8 @@ textarea:focus {
|
||||
|
||||
.user-info {
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-width: calc(100vw - 120px);
|
||||
}
|
||||
|
||||
.user-info .back-btn {
|
||||
@ -2717,7 +2718,7 @@ textarea:focus {
|
||||
/* Дополнительная адаптация для очень маленьких экранов */
|
||||
@media (max-width: 480px) {
|
||||
.user-info {
|
||||
max-width: calc(100vw - 100px);
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@ -2735,6 +2736,16 @@ textarea:focus {
|
||||
.user-info .theme-toggle-btn .iconify {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.settings-icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.settings-icon-btn .iconify {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Адаптируем кнопки markdown */
|
||||
@ -4750,18 +4761,12 @@ textarea:focus {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Фильтры */
|
||||
.filter-indicator {
|
||||
font-size: 12px;
|
||||
display: inline-flex !important;
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
flex: 0 0 auto !important;
|
||||
align-self: flex-start !important;
|
||||
max-width: min(300px, 100%) !important;
|
||||
width: fit-content !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
|
||||
111
vite.config.ts
111
vite.config.ts
@ -8,13 +8,122 @@ export default defineConfig({
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: "prompt",
|
||||
injectRegister: "auto",
|
||||
includeAssets: [
|
||||
"icon.svg",
|
||||
"icons/icon-192x192.png",
|
||||
"icons/icon-512x512.png",
|
||||
],
|
||||
manifest: {
|
||||
name: "NoteJS - Система заметок",
|
||||
short_name: "NoteJS",
|
||||
description:
|
||||
"Современная система заметок с поддержкой Markdown, изображений, тегов и календаря",
|
||||
theme_color: "#007bff",
|
||||
background_color: "#ffffff",
|
||||
display: "standalone",
|
||||
orientation: "portrait-primary",
|
||||
scope: "/",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
{
|
||||
src: "/icons/icon-72x72.png",
|
||||
sizes: "72x72",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-96x96.png",
|
||||
sizes: "96x96",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-128x128.png",
|
||||
sizes: "128x128",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-144x144.png",
|
||||
sizes: "144x144",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-152x152.png",
|
||||
sizes: "152x152",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-384x384.png",
|
||||
sizes: "384x384",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: "/icons/icon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg}"],
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /^https:\/\/api\./,
|
||||
handler: "NetworkFirst",
|
||||
options: {
|
||||
cacheName: "api-cache",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60, // 1 hour
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "images-cache",
|
||||
expiration: {
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
type: "module",
|
||||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
port: 5173,
|
||||
allowedHosts: ["notes.fovway.ru", "localhost"],
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:3001",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user