diff --git a/bun/index.ts b/bun/index.ts
index 3442203..07e72d8 100644
--- a/bun/index.ts
+++ b/bun/index.ts
@@ -13,6 +13,9 @@ const server = Bun.serve({
const url = new URL(req.url);
const page = Math.max(1, Number(url.searchParams.get("page") ?? 1));
+ const UPLOADS_BASE_URL =
+ process.env.WP_UPLOADS_URL ?? "https://new.jkhsakha.ru/wp-content/uploads";
+
// 1. Load grid config post
const gridPost = await connection.run(`
SELECT ID, post_title
@@ -22,16 +25,15 @@ const server = Bun.serve({
`, { id: gridId });
const [grid] = await gridPost.getRowObjectsJson();
- if (!grid) {
- return Response.json(null, { status: 404 });
- }
+ if (!grid) return Response.json(null, { status: 404 });
+
// 2. Load grid meta
const metaRes = await connection.run(`
- SELECT meta_key, meta_value
- FROM wp_postmeta
- WHERE post_id = $id;
- `, { id: gridId });
+ SELECT meta_key, meta_value
+ FROM wp_postmeta
+ WHERE post_id = $id;
+ `, { id: gridId });
const meta = Object.fromEntries(
(await metaRes.getRowObjectsJson())
@@ -47,33 +49,32 @@ const server = Bun.serve({
// 3. Total count (for pagination)
const totalRes = await connection.run(`
- SELECT COUNT(*)::int AS total
- FROM wp_posts
- WHERE post_status = 'publish'
- AND post_type = $postType;
- `, { postType });
+ SELECT COUNT(*)::int AS total
+ FROM wp_posts
+ WHERE post_status = 'publish'
+ AND post_type = $postType;
+ `, { postType });
const [{ total }] = await totalRes.getRowObjectsJson();
const totalPages = Math.max(1, Math.ceil(total / perPage));
-
// clamp page if out of range
const safePage = Math.min(page, totalPages);
const safeOffset = (safePage - 1) * perPage;
// 4. Fetch paginated posts
const postsRes = await connection.run(`
- SELECT
- ID,
- post_title,
- post_name,
- post_excerpt,
- post_date
- FROM wp_posts
- WHERE post_status = 'publish'
- AND post_type = $postType
- ORDER BY ${orderBy} ${order}
- LIMIT $perPage OFFSET $offset;
- `, {
+ SELECT
+ ID,
+ post_title,
+ post_name,
+ post_excerpt,
+ post_date
+ FROM wp_posts
+ WHERE post_status = 'publish'
+ AND post_type = $postType
+ ORDER BY ${orderBy} ${order}
+ LIMIT $perPage OFFSET $offset;
+ `, {
postType,
perPage,
offset: safeOffset
diff --git a/package-lock.json b/package-lock.json
index 6ffc3d0..52b8089 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,15 +10,35 @@
"dependencies": {
"@puckeditor/core": "^0.21.1",
"@tabler/icons-react": "^3.36.1",
+ "@tiptap/extension-code-block-lowlight": "^3.19.0",
+ "@tiptap/extension-file-handler": "^3.20.0",
+ "@tiptap/extension-highlight": "^3.19.0",
+ "@tiptap/extension-image": "^3.19.0",
+ "@tiptap/extension-link": "^3.19.0",
+ "@tiptap/extension-table": "^3.19.0",
+ "@tiptap/extension-task-item": "^3.19.0",
+ "@tiptap/extension-task-list": "^3.19.0",
+ "@tiptap/extension-text-align": "^3.19.0",
+ "@tiptap/extension-typography": "^3.19.0",
+ "@tiptap/extension-underline": "^3.19.0",
+ "@tiptap/extension-youtube": "^3.19.0",
+ "@tiptap/pm": "^3.19.0",
+ "@tiptap/react": "^3.19.0",
+ "@tiptap/starter-kit": "^3.19.0",
"@wordpress/block-serialization-default-parser": "^5.39.0",
"axios": "^1.13.2",
"daisyui": "^5.5.14",
+ "embla-carousel-auto-scroll": "^8.6.0",
+ "embla-carousel-autoplay": "^8.6.0",
+ "embla-carousel-react": "^8.6.0",
"html-react-parser": "^5.2.17",
"interweave": "^13.1.1",
"next": "16.1.3",
"next-themes": "^0.4.6",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "react-resizable-panels": "^4.6.4",
+ "sass": "^1.97.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -640,6 +660,19 @@
"@floating-ui/utils": "^0.2.10"
}
},
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz",
+ "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
@@ -1419,6 +1452,315 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
+ "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3",
+ "is-glob": "^4.0.3",
+ "node-addon-api": "^7.0.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.6",
+ "@parcel/watcher-darwin-arm64": "2.5.6",
+ "@parcel/watcher-darwin-x64": "2.5.6",
+ "@parcel/watcher-freebsd-x64": "2.5.6",
+ "@parcel/watcher-linux-arm-glibc": "2.5.6",
+ "@parcel/watcher-linux-arm-musl": "2.5.6",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.6",
+ "@parcel/watcher-linux-arm64-musl": "2.5.6",
+ "@parcel/watcher-linux-x64-glibc": "2.5.6",
+ "@parcel/watcher-linux-x64-musl": "2.5.6",
+ "@parcel/watcher-win32-arm64": "2.5.6",
+ "@parcel/watcher-win32-ia32": "2.5.6",
+ "@parcel/watcher-win32-x64": "2.5.6"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
+ "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
+ "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
+ "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
+ "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
+ "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
+ "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
+ "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
+ "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
+ "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
+ "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
+ "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
+ "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
+ "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/@preact/signals-core": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.13.0.tgz",
@@ -1609,19 +1951,6 @@
}
}
},
- "node_modules/@puckeditor/core/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@floating-ui/react-dom": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz",
- "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/dom": "^1.7.5"
- },
- "peerDependencies": {
- "react": ">=16.8.0",
- "react-dom": ">=16.8.0"
- }
- },
"node_modules/@puckeditor/core/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@@ -1716,42 +2045,6 @@
}
}
},
- "node_modules/@puckeditor/core/node_modules/@tiptap/react": {
- "version": "3.19.0",
- "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.19.0.tgz",
- "integrity": "sha512-GQQMUUXMpNd8tRjc1jDK3tDRXFugJO7C928EqmeBcBzTKDrFIJ3QUoZKEPxUNb6HWhZ2WL7q00fiMzsv4DNSmg==",
- "license": "MIT",
- "dependencies": {
- "@types/use-sync-external-store": "^0.0.6",
- "fast-equals": "^5.3.3",
- "use-sync-external-store": "^1.4.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "optionalDependencies": {
- "@tiptap/extension-bubble-menu": "^3.19.0",
- "@tiptap/extension-floating-menu": "^3.19.0"
- },
- "peerDependencies": {
- "@tiptap/core": "^3.19.0",
- "@tiptap/pm": "^3.19.0",
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/@puckeditor/core/node_modules/@tiptap/react/node_modules/fast-equals": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
- "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@puckeditor/core/node_modules/react-hotkeys-hook": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.6.2.tgz",
@@ -2309,16 +2602,16 @@
}
},
"node_modules/@tiptap/core": {
- "version": "3.19.0",
- "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
- "integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==",
+ "version": "3.20.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.0.tgz",
+ "integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/pm": "^3.19.0"
+ "@tiptap/pm": "^3.20.0"
}
},
"node_modules/@tiptap/extension-blockquote": {
@@ -2365,6 +2658,19 @@
"@tiptap/pm": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-bullet-list": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.19.0.tgz",
+ "integrity": "sha512-F9uNnqd0xkJbMmRxVI5RuVxwB9JaCH/xtRqOUNQZnRBt7IdAElCY+Dvb4hMCtiNv+enGM/RFGJuFHR9TxmI7rw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.19.0"
+ }
+ },
"node_modules/@tiptap/extension-code": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.19.0.tgz",
@@ -2392,6 +2698,23 @@
"@tiptap/pm": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-code-block-lowlight": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.19.0.tgz",
+ "integrity": "sha512-P8O8i1J+XozEVA7bF/Ijwf/r1rVqrh1DBQ7dXxBcrUvLpIGyVjtxX228jBF/kD4kf2xOlphvjDhV2fLa8XOVsg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.19.0",
+ "@tiptap/extension-code-block": "^3.19.0",
+ "@tiptap/pm": "^3.19.0",
+ "highlight.js": "^11",
+ "lowlight": "^2 || ^3"
+ }
+ },
"node_modules/@tiptap/extension-document": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.19.0.tgz",
@@ -2405,6 +2728,34 @@
"@tiptap/core": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-dropcursor": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.19.0.tgz",
+ "integrity": "sha512-sf3dEZXiLvsGqVK2maUIzXY6qtYYCvBumag7+VPTMGQ0D4hiZ1X/4ukt4+6VXDg5R2WP1CoIt/QvUetUjWNhbQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.19.0"
+ }
+ },
+ "node_modules/@tiptap/extension-file-handler": {
+ "version": "3.20.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-file-handler/-/extension-file-handler-3.20.0.tgz",
+ "integrity": "sha512-o9YTkTjWFAgGOJVzWP+nfmAtT9SDgXj5GmjMIdP4X41k7k+s6iuUtCYheEYZ7yEJZRtZj1tvpTDr8r/zBzia0w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.0",
+ "@tiptap/extension-text-style": "^3.20.0",
+ "@tiptap/pm": "^3.20.0"
+ }
+ },
"node_modules/@tiptap/extension-floating-menu": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.19.0.tgz",
@@ -2421,6 +2772,19 @@
"@tiptap/pm": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-gapcursor": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.19.0.tgz",
+ "integrity": "sha512-w7DACS4oSZaDWjz7gropZHPc9oXqC9yERZTcjWxyORuuIh1JFf0TRYspleK+OK28plK/IftojD/yUDn1MTRhvA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.19.0"
+ }
+ },
"node_modules/@tiptap/extension-hard-break": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz",
@@ -2447,6 +2811,19 @@
"@tiptap/core": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-highlight": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.19.0.tgz",
+ "integrity": "sha512-MYwSDCh/aG12KXw30XmHwrruElBRB8b7Ou0jd8n8H2oXb+QexVqnMa2+ylkuTAl+2D5PR7zdIIOeeHRSTmkPPw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.19.0"
+ }
+ },
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz",
@@ -2461,6 +2838,19 @@
"@tiptap/pm": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-image": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.19.0.tgz",
+ "integrity": "sha512-/rGl8nBziBPVJJ/9639eQWFDKcI3RQsDM3s+cqYQMFQfMqc7sQB9h4o4sHCBpmKxk3Y0FV/0NjnjLbBVm8OKdQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.19.0"
+ }
+ },
"node_modules/@tiptap/extension-italic": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz",
@@ -2505,6 +2895,45 @@
"@tiptap/pm": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-list-item": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.19.0.tgz",
+ "integrity": "sha512-VsSKuJz4/Tb6ZmFkXqWpDYkRzmaLTyE6dNSEpNmUpmZ32sMqo58mt11/huADNwfBFB0Ve7siH/VnFNIJYY3xvg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.19.0"
+ }
+ },
+ "node_modules/@tiptap/extension-list-keymap": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.19.0.tgz",
+ "integrity": "sha512-bxgmAgA3RzBGA0GyTwS2CC1c+QjkJJq9hC+S6PSOWELGRiTbwDN3MANksFXLjntkTa0N5fOnL27vBHtMStURqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.19.0"
+ }
+ },
+ "node_modules/@tiptap/extension-ordered-list": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.19.0.tgz",
+ "integrity": "sha512-cxGsINquwHYE1kmhAcLNLHAofmoDEG6jbesR5ybl7tU5JwtKVO7S/xZatll2DU1dsDAXWPWEeeMl4e/9svYjCg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.19.0"
+ }
+ },
"node_modules/@tiptap/extension-paragraph": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.19.0.tgz",
@@ -2531,6 +2960,46 @@
"@tiptap/core": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-table": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.19.0.tgz",
+ "integrity": "sha512-Lg8DlkkDUMYE/CcGOxoCWF98B2i7VWh+AGgqlF+XWrHjhlKHfENLRXm1a0vWuyyP3NknRYILoaaZ1s7QzmXKRA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.19.0",
+ "@tiptap/pm": "^3.19.0"
+ }
+ },
+ "node_modules/@tiptap/extension-task-item": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-3.19.0.tgz",
+ "integrity": "sha512-1il70SoaoEA5jKr2QS30CpZoB7EzdSLugROMBPRUPc0feIBKAf6yUPhxlFyU4ez/uT4Pazsf1HAgHI3AOr+MtQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.19.0"
+ }
+ },
+ "node_modules/@tiptap/extension-task-list": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-3.19.0.tgz",
+ "integrity": "sha512-Slb6YZi7XpVT966oAJhqzZu4LVuHtUeZgMxA0bdc8FSZBxntcr8OQWmPbqvR437RAR/Xd7b5quXS3JmSeksyvA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.19.0"
+ }
+ },
"node_modules/@tiptap/extension-text": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.19.0.tgz",
@@ -2557,6 +3026,33 @@
"@tiptap/core": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-text-style": {
+ "version": "3.20.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.0.tgz",
+ "integrity": "sha512-zyWW1a6W+kaXAn3wv2svJ1XuVMapujftvH7Xn2Q3QmKKiDkO+NiFkrGe8BhMopu8Im51nO3NylIgVA0X1mS1rQ==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.0"
+ }
+ },
+ "node_modules/@tiptap/extension-typography": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-3.19.0.tgz",
+ "integrity": "sha512-2Rwwz1ErNhqUcXPzPX2u4frdyrK4Yj6ZMvCLPxLt5lQXj9Eq9YEoD9isw8abR105ko3BCidvfElQYSFu6dWPSw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.19.0"
+ }
+ },
"node_modules/@tiptap/extension-underline": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.19.0.tgz",
@@ -2570,6 +3066,33 @@
"@tiptap/core": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-youtube": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-3.19.0.tgz",
+ "integrity": "sha512-/3Z/jw9VbD3WrS1cOcxKUN0oMVnX9NVSekVG4uJHZYs5LhG2tbmUkBYNzj4oJcZcuJ4WPTD1EaAVEgNVffaoJg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.19.0"
+ }
+ },
+ "node_modules/@tiptap/extensions": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.19.0.tgz",
+ "integrity": "sha512-ZmGUhLbMWaGqnJh2Bry+6V4M6gMpUDYo4D1xNux5Gng/E/eYtc+PMxMZ/6F7tNTAuujLBOQKj6D+4SsSm457jw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.19.0",
+ "@tiptap/pm": "^3.19.0"
+ }
+ },
"node_modules/@tiptap/html": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/html/-/html-3.19.0.tgz",
@@ -2586,9 +3109,9 @@
}
},
"node_modules/@tiptap/pm": {
- "version": "3.19.0",
- "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz",
- "integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==",
+ "version": "3.20.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz",
+ "integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
@@ -2615,6 +3138,78 @@
"url": "https://github.com/sponsors/ueberdosis"
}
},
+ "node_modules/@tiptap/react": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.19.0.tgz",
+ "integrity": "sha512-GQQMUUXMpNd8tRjc1jDK3tDRXFugJO7C928EqmeBcBzTKDrFIJ3QUoZKEPxUNb6HWhZ2WL7q00fiMzsv4DNSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "fast-equals": "^5.3.3",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "optionalDependencies": {
+ "@tiptap/extension-bubble-menu": "^3.19.0",
+ "@tiptap/extension-floating-menu": "^3.19.0"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.19.0",
+ "@tiptap/pm": "^3.19.0",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tiptap/react/node_modules/fast-equals": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@tiptap/starter-kit": {
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.19.0.tgz",
+ "integrity": "sha512-dTCkHEz+Y8ADxX7h+xvl6caAj+3nII/wMB1rTQchSuNKqJTOrzyUsCWm094+IoZmLT738wANE0fRIgziNHs/ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@tiptap/core": "^3.19.0",
+ "@tiptap/extension-blockquote": "^3.19.0",
+ "@tiptap/extension-bold": "^3.19.0",
+ "@tiptap/extension-bullet-list": "^3.19.0",
+ "@tiptap/extension-code": "^3.19.0",
+ "@tiptap/extension-code-block": "^3.19.0",
+ "@tiptap/extension-document": "^3.19.0",
+ "@tiptap/extension-dropcursor": "^3.19.0",
+ "@tiptap/extension-gapcursor": "^3.19.0",
+ "@tiptap/extension-hard-break": "^3.19.0",
+ "@tiptap/extension-heading": "^3.19.0",
+ "@tiptap/extension-horizontal-rule": "^3.19.0",
+ "@tiptap/extension-italic": "^3.19.0",
+ "@tiptap/extension-link": "^3.19.0",
+ "@tiptap/extension-list": "^3.19.0",
+ "@tiptap/extension-list-item": "^3.19.0",
+ "@tiptap/extension-list-keymap": "^3.19.0",
+ "@tiptap/extension-ordered-list": "^3.19.0",
+ "@tiptap/extension-paragraph": "^3.19.0",
+ "@tiptap/extension-strike": "^3.19.0",
+ "@tiptap/extension-text": "^3.19.0",
+ "@tiptap/extension-underline": "^3.19.0",
+ "@tiptap/extensions": "^3.19.0",
+ "@tiptap/pm": "^3.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -2633,6 +3228,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2696,6 +3301,13 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -3759,6 +4371,21 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3998,6 +4625,16 @@
"node": ">=0.4.0"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -4014,6 +4651,20 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -4115,6 +4766,52 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/embla-carousel": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
+ "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
+ "license": "MIT"
+ },
+ "node_modules/embla-carousel-auto-scroll": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-auto-scroll/-/embla-carousel-auto-scroll-8.6.0.tgz",
+ "integrity": "sha512-WT9fWhNXFpbQ6kP+aS07oF5IHYLZ1Dx4DkwgCY8Hv2ZyYd2KMCPfMV1q/cA3wFGuLO7GMgKiySLX90/pQkcOdQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.6.0"
+ }
+ },
+ "node_modules/embla-carousel-autoplay": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz",
+ "integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.6.0"
+ }
+ },
+ "node_modules/embla-carousel-react": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz",
+ "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==",
+ "license": "MIT",
+ "dependencies": {
+ "embla-carousel": "8.6.0",
+ "embla-carousel-reactive-utils": "8.6.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/embla-carousel-reactive-utils": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
+ "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.6.0"
+ }
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -5292,6 +5989,16 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/html-dom-parser": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.1.8.tgz",
@@ -5380,6 +6087,12 @@
"node": ">= 4"
}
},
+ "node_modules/immutable": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
+ "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
+ "license": "MIT"
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -5625,7 +6338,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -5671,7 +6384,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -6359,6 +7072,22 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lowlight": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.0.0",
+ "highlight.js": "~11.11.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -6630,6 +7359,13 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -7313,6 +8049,16 @@
}
}
},
+ "node_modules/react-resizable-panels": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.6.4.tgz",
+ "integrity": "sha512-E7Szs1xyaMZ7xOI2gG4TECNz4r/gmpV1AsXyZRnER6OQnfFf9uclFmrHHZR3h/iF8vQS+nQ1LKyZv9bzwGxPSg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -7335,6 +8081,19 @@
}
}
},
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -7516,6 +8275,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/sass": {
+ "version": "1.97.3",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
+ "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.0.2",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
diff --git a/package.json b/package.json
index 292148f..0663076 100644
--- a/package.json
+++ b/package.json
@@ -11,15 +11,35 @@
"dependencies": {
"@puckeditor/core": "^0.21.1",
"@tabler/icons-react": "^3.36.1",
+ "@tiptap/extension-code-block-lowlight": "^3.19.0",
+ "@tiptap/extension-file-handler": "^3.20.0",
+ "@tiptap/extension-highlight": "^3.19.0",
+ "@tiptap/extension-image": "^3.19.0",
+ "@tiptap/extension-link": "^3.19.0",
+ "@tiptap/extension-table": "^3.19.0",
+ "@tiptap/extension-task-item": "^3.19.0",
+ "@tiptap/extension-task-list": "^3.19.0",
+ "@tiptap/extension-text-align": "^3.19.0",
+ "@tiptap/extension-typography": "^3.19.0",
+ "@tiptap/extension-underline": "^3.19.0",
+ "@tiptap/extension-youtube": "^3.19.0",
+ "@tiptap/pm": "^3.19.0",
+ "@tiptap/react": "^3.19.0",
+ "@tiptap/starter-kit": "^3.19.0",
"@wordpress/block-serialization-default-parser": "^5.39.0",
"axios": "^1.13.2",
"daisyui": "^5.5.14",
+ "embla-carousel-auto-scroll": "^8.6.0",
+ "embla-carousel-autoplay": "^8.6.0",
+ "embla-carousel-react": "^8.6.0",
"html-react-parser": "^5.2.17",
"interweave": "^13.1.1",
"next": "16.1.3",
"next-themes": "^0.4.6",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "react-resizable-panels": "^4.6.4",
+ "sass": "^1.97.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
diff --git a/public/karta-s-logo2.png b/public/karta-s-logo2.png
new file mode 100644
index 0000000..4f76f28
Binary files /dev/null and b/public/karta-s-logo2.png differ
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..daef386
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,6 @@
+User-agent: *
+Disallow: /wp-admin/
+Allow: /wp-admin/admin-ajax.php
+Disallow: /wp-content/uploads/wpforms/
+
+Sitemap: https://new.jkhsakha.ru/wp-sitemap.xml
\ No newline at end of file
diff --git a/src/app/[slug]/error.tsx b/src/app/(client)/[slug]/error.tsx
similarity index 100%
rename from src/app/[slug]/error.tsx
rename to src/app/(client)/[slug]/error.tsx
diff --git a/src/app/[slug]/page.tsx b/src/app/(client)/[slug]/page.tsx
similarity index 55%
rename from src/app/[slug]/page.tsx
rename to src/app/(client)/[slug]/page.tsx
index 8cc0d89..cf8747c 100644
--- a/src/app/[slug]/page.tsx
+++ b/src/app/(client)/[slug]/page.tsx
@@ -1,10 +1,11 @@
// app/[slug]/page.tsx
+import Post from '@/components/Post/Post';
import { renderPostContent } from '@/components/WPRenderer/WPRenderer';
// ISR: regenerate every 60 seconds
export const revalidate = 60;
-interface PageProps {
+export interface PageProps {
params: {
slug: string;
};
@@ -28,20 +29,6 @@ export default async function PostPage({ params }: PageProps) {
const post = await res.json();
return (
-
-
- {post.post_title}
-
- {post.post_type === 'post' && (
-
-
- {new Date(post.post_date).toLocaleString('ru-RU')}
-
-
- )}
-
- {renderPostContent(post.post_content)}
-
-
+
);
}
diff --git a/src/app/[slug]/page/[page]/page.tsx b/src/app/(client)/[slug]/page/[page]/page.tsx
similarity index 96%
rename from src/app/[slug]/page/[page]/page.tsx
rename to src/app/(client)/[slug]/page/[page]/page.tsx
index bb121d7..80571aa 100644
--- a/src/app/[slug]/page/[page]/page.tsx
+++ b/src/app/(client)/[slug]/page/[page]/page.tsx
@@ -49,7 +49,7 @@ export default async function PostPage({ params }: PageProps) {
{pageData.post_type === 'post' && (
- {new Date(pageData.post_date).toLocaleString('ru-RU')}
+ {pageData.post_date}
)}
diff --git a/src/app/(client)/layout.tsx b/src/app/(client)/layout.tsx
new file mode 100644
index 0000000..20c793b
--- /dev/null
+++ b/src/app/(client)/layout.tsx
@@ -0,0 +1,39 @@
+import Header from "@/components/Blocks/Header/Header";
+import CookieNotice from "@/components/CookieNotice/CookieNotice";
+import Footer from "@/components/Footer/Footer";
+import "@/styles/globals.css";
+import { Montserrat, Roboto, Roboto_Condensed } from 'next/font/google'
+
+export const montserratFont = Montserrat({
+ subsets: ['latin', 'cyrillic'],
+})
+
+const mainFont = Roboto({
+ subsets: ['latin', 'cyrillic'],
+})
+
+export const condensedFont = Roboto_Condensed({
+ subsets: ['latin', 'cyrillic'],
+})
+
+export const revalidate = 10
+
+export default function ClientLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <>
+
+
+
+ {children}
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(client)/page.tsx b/src/app/(client)/page.tsx
new file mode 100644
index 0000000..3c7b541
--- /dev/null
+++ b/src/app/(client)/page.tsx
@@ -0,0 +1,250 @@
+// app/page.tsx
+import Section from '@/components/Blocks/Section/Section';
+import EmblaCarousel from '@/components/UI/EmblaCarousel/EmblaCarousel';
+import Areas from '@/components/WP/Areas/Areas';
+import { SmartSlider } from '@/components/WPRenderer/SmartSlider';
+import { renderPostContent } from '@/components/WPRenderer/WPRenderer';
+import { CarouselSlide, PostData } from '@/types/entities';
+
+export const revalidate = 10;
+
+export default async function HomePage() {
+ const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
+
+ const res = await fetch(`${baseUrl}/api/home`, { next: { revalidate: 10 }, });
+
+ if (!res.ok) {
+ // 🚨 REQUIRED for stale reuse
+ throw new Error('Failed to fetch home posts');
+ }
+
+ const posts: PostData[] = await res.json();
+
+ const stats: { title: string, value: string }[] = [
+ {
+ title: 'территории Якутии',
+ value: '74 %'
+ },
+ {
+ title: 'муниципальных районов',
+ value: '26'
+ },
+ {
+ title: 'теплоснабжающих объектов',
+ value: '637'
+ },
+ {
+ title: 'Гкал/ч установленная мощность',
+ value: '2645,4'
+ },
+ {
+ title: 'км. инженерных сетей',
+ value: '2818,5'
+ },
+ {
+ title: 'работников',
+ value: '8446'
+ }
+ ]
+
+ const areaSlides: CarouselSlide[] = [
+ {
+ id: 0,
+ title: 'Абыйский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%90%D0%B1%D1%8B%D0%B9%D1%81%D0%BA%D0%B8%D0%B9-pxq84cwhvn5ot9eugai2c8bxn40u3rd9vx6c1fzro0.jpg'
+ },
+ {
+ id: 1,
+ title: 'Аллаиховский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%90%D0%BB%D0%BB%D0%B0%D0%B8%D1%85%D0%BE%D0%B2%D1%81%D0%BA%D0%B8%D0%B9-pxq84gnumzau3p9duc4km7ds0niayjs78fs9yju6z4.jpg'
+ },
+ {
+ id: 3,
+ title: 'Амгинский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%90%D0%BC%D0%B3%D0%B8%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq84kf7ebfze53x8dr2w6fme6zrtc74kye7vnoma8.jpg'
+ },
+ {
+ id: 4,
+ title: 'Анабарский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%90%D0%BD%D0%B0%D0%B1%D0%B0%D1%80%D1%81%D0%BA%D0%B8%D0%B9-pxq84o6k5nl4okygmfdl65hgrqh8o4m1xh05srj1lc.jpg'
+ },
+ {
+ id: 5,
+ title: 'Булунский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%91%D1%83%D0%BB%D1%83%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq84rxwwzq9z0t00h03g4jb59ypix0z9zm3pvdgwg.jpg'
+ },
+ {
+ id: 6,
+ title: 'Верхневилюйский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%92%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B2%D0%B8%D0%BB%D1%8E%D0%B9%D1%81%D0%BA%D0%B8%D0%B9-pxq84vp9obvf9gnjeimlq3l5itg6dpfwmi81mz7w7k.jpg'
+ },
+ {
+ id: 7,
+ title: 'Верхнеколымский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%92%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%BA%D0%BE%D0%BB%D1%8B%D0%BC%D1%81%D0%BA%D0%B8%D0%B9-pxq84zgmfo0kjwi2sk9402mzwcxn8hutz0tzk32bio.jpg'
+ },
+ {
+ id: 8,
+ title: 'Вилюйский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%92%D0%B8%D0%BB%D1%8E%D0%B9%D1%81%D0%BA%D0%B8%D0%B9-pxq857x656c5ge5sf5wr4ii58try5rsf06pcvkpryo.jpg'
+ },
+ {
+ id: 9,
+ title: 'Верхоянский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%92%D0%B5%D1%80%D1%85%D0%BE%D1%8F%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq8545tdu705yb914a8ujgavaahazdhno3eygvcnk.jpg'
+ },
+ {
+ id: 10,
+ title: 'Горный',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%93%D0%BE%D1%80%D0%BD%D1%8B%D0%B9-pxq85cmd3cil2fyynpxvyzbg7r4s89b2otys9yit3k.jpg'
+ },
+ {
+ id: 11,
+ title: 'Жиганский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%96%D0%B8%D0%B3%D0%B0%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq85gdpuonqcvti1rke8ydalam931q01ckq72d8eo.jpg'
+ },
+ {
+ id: 12,
+ title: 'Заречный Кобяй',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%97%D0%B0%D1%80%D0%B5%D1%87%D0%BD%D1%8B%D0%B9-%D0%9A%D0%BE%D0%B1%D1%8F%D0%B9-pxq85k52m0svnbo1ft6wixf4yu3pxu4xdv6o467nps.jpg'
+ },
+ {
+ id: 13,
+ title: 'Кобяйский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%9A%D0%BE%D0%B1%D1%8F%D0%B9%D1%81%D0%BA%D0%B8%D0%B9-pxq85nwfdcy0xriktuteswgzcdl6smjuqdsm1a230w.jpg'
+ },
+ {
+ id: 14,
+ title: 'Мегино-Кангаласский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%9C%D0%B5%D0%B3%D0%B8%D0%BD%D0%BE-%D0%9A%D0%B0%D0%BD%D0%B3%D0%B0%D0%BB%D0%B0%D1%81%D1%81%D0%BA%D0%B8%D0%B9-pxq85rns4p3687d47wfx2vitpx2nneys2wejydwic0.jpg'
+ },
+ {
+ id: 15,
+ title: 'Момский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%9C%D0%BE%D0%BC%D1%81%D0%BA%D0%B8%D0%B9-pxq85vf4w18bin7nly2fcuko3gk4i7dpff0hvhqxn4.jpg'
+ },
+ {
+ id: 16,
+ title: 'Нижнеколымский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%9D%D0%B8%D0%B6%D0%BD%D0%B5%D0%BA%D0%BE%D0%BB%D1%8B%D0%BC%D1%81%D0%BA%D0%B8%D0%B9-pxq8604bu7er4p0tui3k7bdz2dwykowd429x9vjys0.jpg'
+ },
+ {
+ id: 17,
+ title: 'Нюрбинский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%9D%D1%8E%D1%80%D0%B1%D0%B8%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq863voljjwf4vd8jq2haftfxeffhbagkvv6zee34.jpg'
+ },
+ {
+ id: 18,
+ title: 'Олекминский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%9E%D0%BB%D0%B5%D0%BA%D0%BC%D0%B8%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq867n1cvp1pkpwmlckr9hntgvwa9q7t3ht438te8.jpg'
+ },
+ {
+ id: 19,
+ title: 'Оленекский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%9E%D0%BB%D0%B5%D0%BD%D0%B5%D0%BA%D1%81%D0%BA%D0%B8%D0%B9-pxq86bee47u700kg0mz318ji70dd52555m3r1738pc.jpg'
+ },
+ {
+ id: 20,
+ title: 'Среднеколымский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%A1%D1%80%D0%B5%D0%B4%D0%BD%D0%B5%D0%BA%D0%BE%D0%BB%D1%8B%D0%BC%D1%81%D0%BA%D0%B8%D0%B9-pxq86f5qvjzcagezeollb7lckjutzuk2i4poyaxo0g.jpg'
+ },
+ {
+ id: 21,
+ title: 'Сунтарский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%A1%D1%83%D0%BD%D1%82%D0%B0%D1%80%D1%81%D0%BA%D0%B8%D0%B9-pxq86juxtq5rwi85n8mq5oenjh7o2c2q6rz4coqp5c.jpg'
+ },
+ {
+ id: 22,
+ title: 'Таттинский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%A2%D0%B0%D1%82%D1%82%D0%B8%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq86nmal2ax6y2p1a98fnghx0p4x4hnjal29sl4gg.jpg'
+ },
+ {
+ id: 23,
+ title: 'Томпонский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%A2%D0%BE%D0%BC%D0%BF%D0%BE%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq86rdnceg2hdx8fbvqpmicak6lrwwkvt706wfjrk.jpg'
+ },
+ {
+ id: 24,
+ title: 'Усть-Алданский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%A3%D1%81%D1%82%D1%8C-%D0%90%D0%BB%D0%B4%D0%B0%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq86w2uakmi3fqenvwvk3bn9hjfuef8kggfla8kwg.jpg'
+ },
+ {
+ id: 25,
+ title: 'Хангаласский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%A5%D0%B0%D0%BD%D0%B3%D0%B0%D0%BB%D0%B0%D1%81%D1%81%D0%BA%D0%B8%D0%B9-pxq86zu71wrndvky1xjdu2dhn10wp6u5wz2die307k.jpg'
+ },
+ {
+ id: 26,
+ title: 'Чурапчинский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%A7%D1%83%D1%80%D0%B0%D0%BF%D1%87%D0%B8%D0%BD%D1%81%D0%BA%D0%B8%D0%B9-pxq873ljt8wsobfhfz5w41fc0kidjz939hobfhxfio.jpg'
+ },
+ {
+ id: 27,
+ title: 'Эвено-Бытантайский',
+ src: 'https://new.jkhsakha.ru/wp-content/uploads/elementor/thumbs/%D0%AD%D0%B2%D0%B5%D0%BD%D0%BE-%D0%91%D1%8B%D1%82%D0%B0%D0%BD%D1%82%D0%B0%D0%B9%D1%81%D0%BA%D0%B8%D0%B9-pxq877cwkl1xyra0u0see0h6e3zuero0m0a9clruts.jpg'
+ }
+ ]
+
+ const slides: CarouselSlide[] = [
+ {
+ id: 0,
+ title: "Тепло сердец - людям!",
+ src: "/first_slide2.jpg",
+ },
+ {
+ id: 1,
+ title: "Личный кабинет",
+ src: "/Личный-кабинет.jpg",
+ href: "https://lk.jkhsakha.ru",
+ },
+ {
+ id: 2,
+ title: "Способы оплаты",
+ src: "/Способ-оплаты.jpg",
+ href: "/sposoby-oplaty/",
+ }
+ ]
+
+ return (
+
+ {/* {posts.map(post => (
+
+ {renderPostContent(post.post_content)}
+
+ ))} */}
+
+
+
+
+
+
+
+
+
+
+ {stats.map((stat, index) => (
+
+
{stat.value}
+
{stat.title}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(editor)/[slug]/edit/page.tsx b/src/app/(editor)/[slug]/edit/page.tsx
new file mode 100644
index 0000000..b68832c
--- /dev/null
+++ b/src/app/(editor)/[slug]/edit/page.tsx
@@ -0,0 +1,17 @@
+import { PageProps } from "@/app/(client)/[slug]/page";
+import PostPage from "@/app/(client)/[slug]/page";
+import ClientLayout from "@/app/(client)/layout";
+import Post from "@/components/Post/Post";
+import Tiptap from "@/components/Tiptap/Tiptap";
+
+export default async function EditorPage({ params }: PageProps) {
+ const { slug } = await params;
+
+ const pageData = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/pages/${slug}`, {
+ //next: { revalidate: 60 },
+ }).then(res => res.json());
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/(editor)/admin/page.tsx b/src/app/(editor)/admin/page.tsx
new file mode 100644
index 0000000..9667e80
--- /dev/null
+++ b/src/app/(editor)/admin/page.tsx
@@ -0,0 +1,91 @@
+'use client'
+
+import { WebVitalsCard } from "@/components/WebVitalsCard"
+import { useWebVitals } from "@/hooks/useWebVitals"
+
+const AdminPage = () => {
+ const metrics = useWebVitals()
+
+ const vitalsConfig = [
+ {
+ title: 'Time to First Byte (TTFB)',
+ value: metrics.TTFB,
+ unit: 'ms',
+ description: 'Time between request and first byte of response',
+ thresholds: { good: 200, needsImprovement: 500 }
+ },
+ {
+ title: 'First Contentful Paint (FCP)',
+ value: metrics.FCP,
+ unit: 'ms',
+ description: 'Time until first content is rendered',
+ thresholds: { good: 1800, needsImprovement: 3000 }
+ },
+ {
+ title: 'Largest Contentful Paint (LCP)',
+ value: metrics.LCP,
+ unit: 'ms',
+ description: 'Time until largest content element is rendered',
+ thresholds: { good: 2500, needsImprovement: 4000 }
+ },
+ {
+ title: 'First Input Delay (FID)',
+ value: metrics.FID,
+ unit: 'ms',
+ description: 'Time from first user interaction to browser response',
+ thresholds: { good: 100, needsImprovement: 300 }
+ },
+ {
+ title: 'Cumulative Layout Shift (CLS)',
+ value: metrics.CLS,
+ unit: '',
+ description: 'Measures visual stability (lower is better)',
+ thresholds: { good: 0.1, needsImprovement: 0.25 }
+ },
+ {
+ title: 'Interaction to Next Paint (INP)',
+ value: metrics.INP,
+ unit: 'ms',
+ description: 'Measures responsiveness to user interactions',
+ thresholds: { good: 200, needsImprovement: 500 }
+ }
+ ]
+
+ return (
+
+
+
+
Web Vitals Dashboard
+
+ Real-time Core Web Vitals metrics for your Next.js application
+
+
+
+
+ {vitalsConfig.map((vital) => (
+
+ ))}
+
+
+
+
About Core Web Vitals
+
+ Core Web Vitals are a set of real-world, user-centered metrics that quantify key aspects
+ of user experience. They measure dimensions of web usability such as load time,
+ interactivity, and the stability of content as it loads.
+
+
+
+
+ )
+
+}
+
+export default AdminPage
\ No newline at end of file
diff --git a/src/app/(editor)/admin/posts/page.tsx b/src/app/(editor)/admin/posts/page.tsx
new file mode 100644
index 0000000..dc0fff1
--- /dev/null
+++ b/src/app/(editor)/admin/posts/page.tsx
@@ -0,0 +1,354 @@
+import React from 'react'
+
+const PostsPage = () => {
+ return (
+
+
+
+ {/* head */}
+
+
+
+
+
+
+
+ Name
+ Job
+ Favorite Color
+
+
+
+
+ {/* row 1 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Hart Hagerty
+
United States
+
+
+
+
+ Zemlak, Daniel and Leannon
+
+ Desktop Support Technician
+
+ Purple
+
+ details
+
+
+ {/* row 2 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Carroll Group
+
+ Tax Accountant
+
+ Red
+
+ details
+
+
+ {/* row 3 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Marjy Ferencz
+
Russia
+
+
+
+
+ Rowe-Schoen
+
+ Office Assistant I
+
+ Crimson
+
+ details
+
+
+ {/* row 4 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wyman-Ledner
+
+ Community Outreach Specialist
+
+ Indigo
+
+ details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wyman-Ledner
+
+ Community Outreach Specialist
+
+ Indigo
+
+ details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wyman-Ledner
+
+ Community Outreach Specialist
+
+ Indigo
+
+ details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wyman-Ledner
+
+ Community Outreach Specialist
+
+ Indigo
+
+ details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wyman-Ledner
+
+ Community Outreach Specialist
+
+ Indigo
+
+ details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wyman-Ledner
+
+ Community Outreach Specialist
+
+ Indigo
+
+ details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wyman-Ledner
+
+ Community Outreach Specialist
+
+ Indigo
+
+ details
+
+
+
+ {/* foot */}
+
+
+
+ Name
+ Job
+ Favorite Color
+
+
+
+
+
+
+ )
+}
+
+export default PostsPage
\ No newline at end of file
diff --git a/src/app/(editor)/layout.tsx b/src/app/(editor)/layout.tsx
new file mode 100644
index 0000000..f7f224b
--- /dev/null
+++ b/src/app/(editor)/layout.tsx
@@ -0,0 +1,70 @@
+import { IconBook2, IconHome, IconSettings } from "@tabler/icons-react";
+
+export default function EditLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {/* Navbar */}
+
+
+ {/* Sidebar toggle icon */}
+
+
+
+ {/* Editor Navbar title goes here */}
+
+
+ {/* Editor Navbar items go here */}
+
+
+ {/* Page content here */}
+
+ {children}
+
+
+
+
+
+
+ {/* Sidebar content here */}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index caa7aaf..47b784b 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,3 @@
-import Header from "@/components/Blocks/Header/Header";
-import Footer from "@/components/Footer/Footer";
import "@/styles/globals.css";
import { Montserrat, Roboto, Roboto_Condensed } from 'next/font/google'
@@ -25,13 +23,16 @@ export default function GlobalLayout({
return (
-
+ {/*
{children}
+
+ */}
+ {children}
);
diff --git a/src/app/page.tsx b/src/app/page.tsx
deleted file mode 100644
index b389f98..0000000
--- a/src/app/page.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-// app/page.tsx
-import { renderPostContent } from '@/components/WPRenderer/WPRenderer';
-import { PostData } from '@/types/entities';
-
-export const revalidate = 10;
-
-export default async function HomePage() {
- const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
-
- const res = await fetch(`${baseUrl}/api/home`, { next: { revalidate: 10 }, });
-
- if (!res.ok) {
- // 🚨 REQUIRED for stale reuse
- throw new Error('Failed to fetch home posts');
- }
-
- const posts: PostData[] = await res.json();
-
- return (
-
- {posts.map(post => (
-
- {renderPostContent(post.post_content)}
-
- ))}
-
- );
-}
diff --git a/src/app/test/page.tsx b/src/app/test/page.tsx
new file mode 100644
index 0000000..54bc61b
--- /dev/null
+++ b/src/app/test/page.tsx
@@ -0,0 +1,13 @@
+// app/page.tsx
+
+import Areas from "@/components/WP/Areas/Areas";
+
+export const revalidate = 10;
+
+export default async function TestPage() {
+ return (
+
+ );
+}
diff --git a/src/components/Blocks/Header/Header.tsx b/src/components/Blocks/Header/Header.tsx
index 540a766..1c08a3e 100644
--- a/src/components/Blocks/Header/Header.tsx
+++ b/src/components/Blocks/Header/Header.tsx
@@ -8,8 +8,7 @@ import { ContentScheme } from '@/types/elements';
export const revalidate = 60;
async function Header() {
- const baseUrl =
- process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
+ const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
const res = await fetch(`${baseUrl}/api/menu/navbar`, {
next: { revalidate: 60 },
diff --git a/src/components/Blocks/HomeStats/HomeStats.tsx b/src/components/Blocks/HomeStats/HomeStats.tsx
new file mode 100644
index 0000000..f6750c2
--- /dev/null
+++ b/src/components/Blocks/HomeStats/HomeStats.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+
+const HomeStats = () => {
+ return (
+
+
+
+ )
+}
+
+export default HomeStats
\ No newline at end of file
diff --git a/src/components/Blocks/Section/Section.tsx b/src/components/Blocks/Section/Section.tsx
new file mode 100644
index 0000000..0832f35
--- /dev/null
+++ b/src/components/Blocks/Section/Section.tsx
@@ -0,0 +1,21 @@
+import { montserratFont } from '@/app/layout'
+import React, { PropsWithChildren } from 'react'
+
+interface SectionProps extends PropsWithChildren {
+ title?: string
+}
+
+const Section = ({ children, title }: SectionProps) => {
+ return (
+
+ {title &&
+
{title}
+ }
+
+ {children}
+
+
+ )
+}
+
+export default Section
\ No newline at end of file
diff --git a/src/components/CookieNotice/CookieNotice.tsx b/src/components/CookieNotice/CookieNotice.tsx
new file mode 100644
index 0000000..30a8549
--- /dev/null
+++ b/src/components/CookieNotice/CookieNotice.tsx
@@ -0,0 +1,51 @@
+import { IconX } from '@tabler/icons-react'
+import React from 'react'
+
+interface CookieNoticeButton {
+ label: string;
+ action: () => void;
+}
+
+interface CookieNotice {
+ buttons: CookieNoticeButton[];
+ message: string;
+ order: number
+}
+
+const CookieNotice = () => {
+ const cookieNoticeData: CookieNotice = {
+ order: 0,
+ message: "Продолжая использовать сайт, Вы принимаете условия ПОЛИТИКИ и даёте согласие на обработку пользовательских данных (файлов cookie).",
+ buttons: [
+ {
+ label: "Понятно",
+ action: () => {
+ // Логика принятия cookies
+ console.log("Cookies accepted");
+ }
+ },
+ {
+ label: "Отклонить",
+ action: () => {
+ // Логика отклонения cookies
+ console.log("Cookies rejected");
+ }
+ }
+ ]
+ };
+
+ return (
+
+ {/*
+ {cookieNoticeData.buttons.map((button, index) => (
+
+ {button.label}
+
+ ))} */}
+ {/*
*/}
+
+
+ )
+}
+
+export default CookieNotice
\ No newline at end of file
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
index 8944380..c52cf37 100644
--- a/src/components/Footer/Footer.tsx
+++ b/src/components/Footer/Footer.tsx
@@ -4,9 +4,9 @@ import { IconBrandOkRu, IconBrandTelegram, IconBrandVk } from '@tabler/icons-rea
const Footer = () => {
return (
-
+
-
+
О предприятии
Руководство
diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx
new file mode 100644
index 0000000..8d0de76
--- /dev/null
+++ b/src/components/Post/Post.tsx
@@ -0,0 +1,24 @@
+import { renderPostContent } from '../WPRenderer/WPRenderer'
+import { PostData } from '@/types/entities'
+
+const Post = ({ post }: { post: PostData }) => {
+ return (
+
+
+ {post.post_title}
+
+ {post.post_type === 'post' && (
+
+
+ {post.post_date}
+
+
+ )}
+
+ {renderPostContent(post.post_content)}
+
+
+ )
+}
+
+export default Post
\ No newline at end of file
diff --git a/src/components/Post/PostSettings.tsx b/src/components/Post/PostSettings.tsx
new file mode 100644
index 0000000..01d4a8c
--- /dev/null
+++ b/src/components/Post/PostSettings.tsx
@@ -0,0 +1,25 @@
+import { PostData } from '@/types/entities'
+import React from 'react'
+
+const PostSettings = ({
+ data
+}: {
+ data: PostData
+}) => {
+ return (
+
+
+
+
Категории
+
+
+
+ Рубрики
+
+
+
+
+ )
+}
+
+export default PostSettings
\ No newline at end of file
diff --git a/src/components/Tiptap/Tiptap.tsx b/src/components/Tiptap/Tiptap.tsx
new file mode 100644
index 0000000..4a13cca
--- /dev/null
+++ b/src/components/Tiptap/Tiptap.tsx
@@ -0,0 +1,218 @@
+'use client'
+
+import { useEditor, EditorContent, ResizableNodeView } from '@tiptap/react'
+import { FloatingMenu, BubbleMenu } from '@tiptap/react/menus'
+import StarterKit from '@tiptap/starter-kit'
+import { useEffect, useRef, useState } from 'react'
+import Post from '../Post/Post'
+import { PostData } from '@/types/entities'
+import TiptapToolbar from './TiptapToolbar'
+import Text from '@tiptap/extension-text'
+import Image from '@tiptap/extension-image'
+import Paragraph from '@tiptap/extension-paragraph'
+import { TaskItem, TaskList } from '@tiptap/extension-list'
+import { Highlight } from '@tiptap/extension-highlight'
+import FileHandler from '@tiptap/extension-file-handler'
+import Underline from '@tiptap/extension-underline'
+import Link from '@tiptap/extension-link'
+import TextAlign from '@tiptap/extension-text-align'
+import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table';
+import Typography from '@tiptap/extension-typography';
+import { Group, Panel, usePanelRef } from 'react-resizable-panels'
+import { createPortal } from 'react-dom'
+import PostSettings from '../Post/PostSettings'
+import { Dropcursor } from '@tiptap/extensions'
+import TiptapBubbleMenu from './TiptapBubbleMenu'
+import TiptapFloatingMenu from './TiptapFloatingMenu'
+
+const Tiptap = ({
+ slug,
+ data
+}: {
+ slug: string
+ data: PostData
+}) => {
+ const [content, setContent] = useState(data.post_content)
+
+ const [showPreview, setShowPreview] = useState(true)
+
+ const previewModal = useRef(null)
+
+ const editor = useEditor({
+ editorProps: {
+ attributes: {
+ class: 'prose lg:prose-xl p-4 focus:outline-none max-w-none',// focus:outline-none',
+ },
+ },
+ extensions: [
+ StarterKit.configure({
+ codeBlock: false
+ }),
+ Link.configure({
+ openOnClick: false
+ }),
+ Image.configure({
+ resize: {
+ enabled: true,
+ alwaysPreserveAspectRatio: true,
+ },
+ inline: false,
+ }),
+ TextAlign.configure({
+ types: ['heading', 'paragraph'],
+ }),
+ TaskList,
+ TaskItem.configure({
+ nested: true
+ }),
+ Table.configure({
+ resizable: true
+ }),
+ FileHandler.configure({
+ allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
+ onDrop: (currentEditor, files, pos) => {
+ files.forEach(file => {
+ const fileReader = new FileReader()
+
+ fileReader.readAsDataURL(file)
+ fileReader.onload = () => {
+ currentEditor
+ .chain()
+ .insertContentAt(pos, {
+ type: 'image',
+ attrs: {
+ src: fileReader.result,
+ },
+ })
+ .focus()
+ .run()
+ }
+ })
+ },
+ onPaste: (currentEditor, files, htmlContent) => {
+ files.forEach(file => {
+ if (htmlContent) {
+ // if there is htmlContent, stop manual insertion & let other extensions handle insertion via inputRule
+ // you could extract the pasted file from this url string and upload it to a server for example
+ console.log(htmlContent) // eslint-disable-line no-console
+ return false
+ }
+
+ const fileReader = new FileReader()
+
+ fileReader.readAsDataURL(file)
+ fileReader.onload = () => {
+ currentEditor
+ .chain()
+ .insertContentAt(currentEditor.state.selection.anchor, {
+ type: 'image',
+ attrs: {
+ src: fileReader.result,
+ },
+ })
+ .focus()
+ .run()
+ }
+ })
+ },
+ }),
+ TableRow, TableHeader, TableCell,
+ Highlight,
+ Typography,
+ Underline,
+ Text,
+ Paragraph,
+ Dropcursor
+ ],
+ content: content,
+ // Don't render immediately on the server to avoid SSR issues
+ immediatelyRender: false,
+ onUpdate: (({ editor }) => {
+ setContent(editor.getHTML())
+ }),
+ })
+
+ useEffect(() => {
+ if (editor && content !== editor.getHTML()) {
+ editor.commands.setContent(content);
+ }
+ }, [content, editor]);
+
+ const editorNavbar = useRef(null)
+ const editorNavbarTitle = useRef(null)
+
+ useEffect(() => {
+ if (document) {
+ editorNavbar.current = document.getElementById("editor-navbar")
+ editorNavbarTitle.current = document.getElementById("editor-navbar-title")
+ }
+ }, [])
+
+ const rightPanelRef = usePanelRef()
+
+ return (
+
+ {editorNavbarTitle.current &&
+ createPortal(
{data.post_title} , editorNavbarTitle.current)
+ }
+
+ {editorNavbar.current &&
+ createPortal(
+
+ {
+ if (previewModal) {
+ previewModal.current?.showModal()
+ }
+ }}>
+ Предпросмотр
+
+ {
+ if (previewModal) {
+ previewModal.current?.showModal()
+ }
+ }}>
+ Опубликовать
+
+
+ , editorNavbar.current)}
+
+
+
+
+ {editor &&
+
+ }
+
+ {editor ?
+
+
+
+
+
+ :
+
+ }
+
+
+
+
+
+
+
+
+ {
+
+
+
+ }
+
+
+ )
+}
+
+export default Tiptap
diff --git a/src/components/Tiptap/TiptapBubbleMenu.tsx b/src/components/Tiptap/TiptapBubbleMenu.tsx
new file mode 100644
index 0000000..a7bf6eb
--- /dev/null
+++ b/src/components/Tiptap/TiptapBubbleMenu.tsx
@@ -0,0 +1,46 @@
+import { BubbleMenu } from '@tiptap/react/menus'
+import React from 'react'
+import { Editor, useEditorState } from '@tiptap/react';
+import { tiptapToolBarStateSelector } from './TiptapToolbarState';
+import ToolbarButton from './ToolbarButton';
+import { IconBold, IconCode, IconItalic, IconStrikethrough } from '@tabler/icons-react';
+
+const TiptapBubbleMenu = ({
+ editor
+}: {
+ editor: Editor
+}) => {
+ if (!editor) {
+ return null;
+ }
+
+ const editorState = useEditorState({
+ editor,
+ selector: tiptapToolBarStateSelector,
+ })
+
+
+ return (
+
+ {<>
+ {editorState.canBold && } onClick={() => editor.chain().focus().toggleBold().run()} />}
+ {editorState.canItalic && } onClick={() => editor.chain().focus().toggleItalic().run()} />}
+ {editorState.canStrike && } onClick={() => editor.chain().focus().toggleStrike().run()} />}
+ {editorState.canCode && }
+ onClick={() => editor.chain().focus().toggleCode().run()}
+ />}
+ >}
+
+
+ )
+}
+
+export default TiptapBubbleMenu
\ No newline at end of file
diff --git a/src/components/Tiptap/TiptapFloatingMenu.tsx b/src/components/Tiptap/TiptapFloatingMenu.tsx
new file mode 100644
index 0000000..bd5b0e0
--- /dev/null
+++ b/src/components/Tiptap/TiptapFloatingMenu.tsx
@@ -0,0 +1,53 @@
+import { Editor, useEditorState } from '@tiptap/react'
+import { FloatingMenu } from '@tiptap/react/menus'
+import { tiptapToolBarStateSelector } from './TiptapToolbarState';
+import ToolbarButton from './ToolbarButton';
+import { addHorizontalRule, addImage, addTable } from './tiptapActions';
+import { IconMinus, IconPolaroid, IconSeparatorHorizontal, IconTable } from '@tabler/icons-react';
+
+const TiptapFloatingMenu = ({
+ editor
+}: {
+ editor: Editor
+}) => {
+ if (!editor) {
+ return null;
+ }
+
+ const editorState = useEditorState({
+ editor,
+ selector: tiptapToolBarStateSelector,
+ })
+
+ return (
+
+ }
+ onClick={() => addImage(editor)}
+ />
+
+ }
+ onClick={() => addTable(editor)}
+ />
+
+ }
+ onClick={() => addHorizontalRule(editor)}
+ />
+
+ }
+ onClick={() => editor.chain().focus().setHardBreak().run()}
+ />
+
+ )
+}
+
+export default TiptapFloatingMenu
\ No newline at end of file
diff --git a/src/components/Tiptap/TiptapToolbar.tsx b/src/components/Tiptap/TiptapToolbar.tsx
new file mode 100644
index 0000000..e7dd61a
--- /dev/null
+++ b/src/components/Tiptap/TiptapToolbar.tsx
@@ -0,0 +1,222 @@
+// components/TiptapToolbar.tsx
+import { IconAlignCenter, IconAlignLeft, IconAlignRight, IconArrowBack, IconArrowForward, IconBold, IconClearFormatting, IconClipboard, IconCode, IconCodeDots, IconH1, IconH2, IconH3, IconItalic, IconLink, IconList, IconListCheck, IconListNumbers, IconMinus, IconPilcrow, IconPolaroid, IconQuote, IconSeparatorHorizontal, IconSparkles, IconStrikethrough, IconTable, IconUnlink } from '@tabler/icons-react';
+import { Editor, useEditorState } from '@tiptap/react';
+import { tiptapToolBarStateSelector } from './TiptapToolbarState';
+import { useEffect, useRef } from 'react';
+import { createPortal } from 'react-dom';
+import ToolbarButton from './ToolbarButton';
+import { addHorizontalRule, addImage, addTable, setLink } from './tiptapActions';
+
+interface ToolbarProps {
+ editor: Editor | null;
+}
+
+const TiptapToolbar = ({ editor }: ToolbarProps) => {
+ if (!editor) {
+ return null;
+ }
+
+ const editorState = useEditorState({
+ editor,
+ selector: tiptapToolBarStateSelector,
+ })
+
+ const editorNavbar = useRef(null)
+
+ useEffect(() => {
+ if (document) {
+ editorNavbar.current = document.getElementById("editor-navbar")
+ }
+ }, [])
+
+ return (
+
+ {/* Text Formatting */}
+
+ } onClick={() => editor.chain().focus().toggleBold().run()} />
+ } onClick={() => editor.chain().focus().toggleItalic().run()} />
+ } onClick={() => editor.chain().focus().toggleStrike().run()} />
+ }
+ onClick={() => editor.chain().focus().toggleCode().run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().unsetAllMarks().run()}
+ />
+
+
+ {/* Headings */}
+
+ }
+ onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().setParagraph().run()}
+ />
+
+
+ {/* Lists */}
+
+ }
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
+ />
+
+
+ {/* Alignment */}
+
+ }
+ onClick={() => editor.chain().focus().setTextAlign('left').run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().setTextAlign('center').run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().setTextAlign('right').run()}
+ />
+
+
+ {/* Media & Links */}
+
+ }
+ onClick={() => addImage(editor)}
+ />
+
+ }
+ onClick={() => setLink(editor)}
+ />
+
+ }
+ disabled={!editorState.isLink}
+ onClick={() => editor.chain().focus().unsetLink().run()}
+ />
+
+
+ {/* Advanced Features */}
+
+ }
+ onClick={() => addTable(editor)}
+ />
+
+ }
+ onClick={() => editor.chain().focus().toggleCodeBlock().run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().toggleBlockquote().run()}
+ />
+
+ }
+ onClick={() => addHorizontalRule(editor)}
+ />
+
+ }
+ onClick={() => editor.chain().focus().setHardBreak().run()}
+ />
+
+
+ {/* History */}
+ {editorNavbar.current ?
+ createPortal(
+ }
+ onClick={() => editor.chain().focus().undo().run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().redo().run()}
+ />
+
, editorNavbar.current) :
+
+ }
+ onClick={() => editor.chain().focus().undo().run()}
+ />
+
+ }
+ onClick={() => editor.chain().focus().redo().run()}
+ />
+
+ }
+
+ );
+};
+
+export default TiptapToolbar;
\ No newline at end of file
diff --git a/src/components/Tiptap/TiptapToolbarState.ts b/src/components/Tiptap/TiptapToolbarState.ts
new file mode 100644
index 0000000..3e8ec35
--- /dev/null
+++ b/src/components/Tiptap/TiptapToolbarState.ts
@@ -0,0 +1,43 @@
+import { Editor, EditorStateSnapshot } from "@tiptap/react"
+
+export function tiptapToolBarStateSelector(ctx: EditorStateSnapshot) {
+ return {
+ // Text formatting
+ isBold: ctx.editor.isActive('bold') ?? false,
+ canBold: ctx.editor.can().chain().toggleBold().run() ?? false,
+ isItalic: ctx.editor.isActive('italic') ?? false,
+ canItalic: ctx.editor.can().chain().toggleItalic().run() ?? false,
+ isStrike: ctx.editor.isActive('strike') ?? false,
+ canStrike: ctx.editor.can().chain().toggleStrike().run() ?? false,
+ isCode: ctx.editor.isActive('code') ?? false,
+ canCode: ctx.editor.can().chain().toggleCode().run() ?? false,
+ canClearMarks: ctx.editor.can().chain().unsetAllMarks().run() ?? false,
+ isLink: ctx.editor.isActive('link') ?? false,
+
+ // Block types
+ isParagraph: ctx.editor.isActive('paragraph') ?? false,
+ isHeading1: ctx.editor.isActive('heading', { level: 1 }) ?? false,
+ isHeading2: ctx.editor.isActive('heading', { level: 2 }) ?? false,
+ isHeading3: ctx.editor.isActive('heading', { level: 3 }) ?? false,
+ isHeading4: ctx.editor.isActive('heading', { level: 4 }) ?? false,
+ isHeading5: ctx.editor.isActive('heading', { level: 5 }) ?? false,
+ isHeading6: ctx.editor.isActive('heading', { level: 6 }) ?? false,
+
+ // Lists and blocks
+ isBulletList: ctx.editor.isActive('bulletList') ?? false,
+ isOrderedList: ctx.editor.isActive('orderedList') ?? false,
+ isCodeBlock: ctx.editor.isActive('codeBlock') ?? false,
+ isBlockquote: ctx.editor.isActive('blockquote') ?? false,
+
+ // Align
+ isAlignLeft: ctx.editor.isActive({textAlign: 'left'}) ?? false,
+ isAlignRight: ctx.editor.isActive({textAlign: 'right'}) ?? false,
+ isAlignCenter: ctx.editor.isActive({textAlign: 'center'}) ?? false,
+
+ // History
+ canUndo: ctx.editor.can().chain().undo().run() ?? false,
+ canRedo: ctx.editor.can().chain().redo().run() ?? false,
+ }
+}
+
+export type TiptapToolbarState = ReturnType
diff --git a/src/components/Tiptap/ToolbarButton.tsx b/src/components/Tiptap/ToolbarButton.tsx
new file mode 100644
index 0000000..ceb2ee0
--- /dev/null
+++ b/src/components/Tiptap/ToolbarButton.tsx
@@ -0,0 +1,28 @@
+import React from 'react'
+
+const ToolbarButton = ({
+ isActive,
+ onClick,
+ icon,
+ title,
+ disabled
+}: {
+ isActive?: boolean
+ onClick: () => void
+ icon?: React.ReactNode
+ title?: string
+ disabled?: boolean
+}) => {
+ return (
+
+ {icon ?? icon}
+
+ )
+}
+
+export default ToolbarButton
\ No newline at end of file
diff --git a/src/components/Tiptap/tiptapActions.ts b/src/components/Tiptap/tiptapActions.ts
new file mode 100644
index 0000000..817fa05
--- /dev/null
+++ b/src/components/Tiptap/tiptapActions.ts
@@ -0,0 +1,36 @@
+import { Editor } from "@tiptap/react";
+
+export const addImage = (editor: Editor) => {
+ const url = window.prompt('Enter image URL:');
+ if (url) {
+ editor.chain().focus().setImage({ src: url }).run();
+ }
+};
+
+export const addHorizontalRule = (editor: Editor) => {
+ editor.chain().focus().setHorizontalRule().run();
+};
+
+export const addTable = (editor: Editor) => {
+ editor
+ .chain()
+ .focus()
+ .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
+ .run();
+};
+
+export const setLink = (editor: Editor) => {
+ const previousUrl = editor.getAttributes('link').href;
+ const url = window.prompt('Enter URL:', previousUrl);
+
+ if (url === null) {
+ return;
+ }
+
+ if (url === '') {
+ editor.chain().focus().unsetLink().run();
+ return;
+ }
+
+ editor.chain().focus().setLink({ href: url }).run();
+};
\ No newline at end of file
diff --git a/src/components/UI/EmblaCarousel/EmblaCarousel.css b/src/components/UI/EmblaCarousel/EmblaCarousel.css
new file mode 100644
index 0000000..15ed10b
--- /dev/null
+++ b/src/components/UI/EmblaCarousel/EmblaCarousel.css
@@ -0,0 +1,62 @@
+.embla {
+ position: relative;
+}
+
+.embla__viewport {
+ overflow: hidden;
+}
+
+.embla__container {
+ display: flex;
+ touch-action: pan-y pinch-zoom;
+}
+
+.embla__slide {
+ display: flex;
+ flex: 0 0 100%;
+ min-width: 0;
+ justify-content: center;
+}
+
+/* Navigation buttons */
+.embla__buttons {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.embla__button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.5rem;
+ border: none;
+ cursor: pointer;
+}
+
+.embla__button:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+/* Dots navigation */
+.embla__dots {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.embla__dot {
+ width: 0.75rem;
+ height: 0.75rem;
+ border-radius: 9999px;
+ background-color: rgba(0, 0, 0, 0.2);
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.embla__dot--selected {
+ background-color: #0063A7;
+ width: 1.5rem;
+ /* Makes the selected dot wider */
+}
\ No newline at end of file
diff --git a/src/components/UI/EmblaCarousel/EmblaCarousel.tsx b/src/components/UI/EmblaCarousel/EmblaCarousel.tsx
new file mode 100644
index 0000000..32798bf
--- /dev/null
+++ b/src/components/UI/EmblaCarousel/EmblaCarousel.tsx
@@ -0,0 +1,227 @@
+'use client'
+import './EmblaCarousel.css'
+import React, { HTMLAttributes, useCallback, useEffect, useRef, useState } from 'react'
+import useEmblaCarousel from 'embla-carousel-react'
+import Autoplay from 'embla-carousel-autoplay'
+import AutoScroll from 'embla-carousel-auto-scroll'
+import { CarouselSlide } from '@/types/entities'
+import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'
+import { montserratFont } from '@/app/layout'
+
+interface EmblaCarouselProps {
+ slides: CarouselSlide[]
+ slides_per_view?: number
+ show_dots?: boolean
+ dots_position?: 'bottom_inside' | 'bottom'
+ buttons_position?: 'middle_inside' | 'bottom'
+ show_title?: boolean
+ title_align?: 'center' | 'left' | 'right'
+ title_position?: 'middle' | 'top' | 'bottom'
+ autoplay?: boolean
+ autoscroll?: boolean
+ autoscroll_speed?: number
+ autoscroll_direction?: 'forward' | 'backward'
+ shape?: 'fit' | 'square'
+ rounded?: boolean
+}
+
+const EmblaCarousel = ({
+ slides,
+ slides_per_view,
+ show_dots = false,
+ dots_position = 'bottom_inside',
+ buttons_position = 'middle_inside',
+ show_title = false,
+ title_align = 'center',
+ title_position = 'middle',
+ autoplay = false,
+ autoscroll = false,
+ autoscroll_speed = 1.5,
+ autoscroll_direction = 'forward',
+ shape = 'fit',
+ rounded = false
+}: EmblaCarouselProps) => {
+ const autoScrollPlugin = useRef(
+ AutoScroll({
+ active: autoscroll,
+ speed: autoscroll_speed,
+ direction: autoscroll_direction,
+ stopOnInteraction: false, // Stop when user interacts
+ stopOnMouseEnter: false, // Stop on hover
+ //startDelay: 500, // Delay before starting
+ playOnInit: autoscroll, // Start playing on init
+ })
+ )
+
+ const autoplayPlugin = useRef(
+ Autoplay({
+ active: autoplay,
+ stopOnInteraction: false, // Stop when user interacts
+ stopOnMouseEnter: false, // Stop on hover
+ jump: false, // false = smooth scroll, true = instant jump
+ playOnInit: autoplay, // Start playing on init
+ })
+ )
+
+ const [emblaRef, emblaApi] = useEmblaCarousel({
+ align: 'start',
+ loop: true
+ }, [autoplayPlugin.current, autoScrollPlugin.current])
+
+ const [selectedIndex, setSelectedIndex] = useState(0)
+ const [scrollSnaps, setScrollSnaps] = useState([])
+
+ const scrollTo = useCallback((index: number) => {
+ emblaApi?.scrollTo(index)
+
+ autoScrollPlugin.current.stop()
+ setTimeout(() => {
+ if (autoscroll) autoScrollPlugin.current.play()
+ }, 1000)
+ }, [emblaApi])
+
+ const scrollPrev = useCallback(() => {
+ emblaApi?.scrollPrev()
+
+ autoScrollPlugin.current.stop()
+ setTimeout(() => {
+ if (autoscroll) autoScrollPlugin.current.play()
+ }, 1000)
+ }, [emblaApi])
+
+ const scrollNext = useCallback(() => {
+ emblaApi?.scrollNext()
+
+ autoScrollPlugin.current.stop()
+ setTimeout(() => {
+ if (autoscroll) autoScrollPlugin.current.play()
+ }, 1000)
+ }, [emblaApi])
+
+ const onSelect = useCallback(() => {
+ if (!emblaApi) return
+ setSelectedIndex(emblaApi.selectedScrollSnap())
+ }, [emblaApi])
+
+ useEffect(() => {
+ if (!emblaApi) return
+
+ onSelect()
+ setScrollSnaps(emblaApi.scrollSnapList())
+ emblaApi.on('select', onSelect)
+ emblaApi.on('reInit', onSelect)
+
+ if (autoplay) {
+ autoplayPlugin.current.play()
+ } else {
+ autoplayPlugin.current.stop()
+ }
+
+ if (autoscroll) {
+ autoScrollPlugin.current.play()
+ } else {
+ autoplayPlugin.current.stop()
+ }
+
+ return () => {
+ emblaApi.off('select', onSelect)
+ emblaApi.off('reInit', onSelect)
+ emblaApi.off('autoScroll:play', () => { })
+ emblaApi.off('autoScroll:stop', () => { })
+ }
+ }, [emblaApi, autoplay, autoscroll, onSelect])
+
+ const slideTitleAlign = (title_align?: EmblaCarouselProps['title_align']) => {
+ switch (title_align) {
+ case 'center':
+ return 'justify-center'
+ case 'left':
+ return 'justify-start'
+ case 'right':
+ return 'justify-end'
+ default:
+ return ''
+ }
+ }
+
+ const slideTitlePosition = (title_position?: EmblaCarouselProps['title_position']) => {
+ switch (title_position) {
+ case 'bottom':
+ return 'items-end'
+ case 'middle':
+ return 'items-center'
+ case 'top':
+ return 'items-start'
+ default:
+ return ''
+ }
+ }
+
+ const slideClass = (slides_per_view?: number) => {
+ switch (slides_per_view) {
+ case 1:
+ return ''
+ case 2:
+ return 'sm:basis-1/2!'
+ case 3:
+ return 'sm:basis-1/3!'
+ case 4:
+ return 'basis-1/2! sm:basis-1/4!'
+ default:
+ return ''
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {show_dots &&
+
+ {scrollSnaps.map((_, index) => (
+ scrollTo(index)}
+ aria-label={`Go to slide ${index + 1}`}
+ />
+ ))}
+
+
+
}
+
+ )
+}
+
+export default EmblaCarousel
\ No newline at end of file
diff --git a/src/components/WP/Areas/Areas.tsx b/src/components/WP/Areas/Areas.tsx
new file mode 100644
index 0000000..f18d6a3
--- /dev/null
+++ b/src/components/WP/Areas/Areas.tsx
@@ -0,0 +1,214 @@
+'use client'
+
+import { useEffect, useRef, useState } from "react"
+
+export const revalidate = 1000
+
+interface Area {
+ name: string
+ svgPath: string
+ href: string
+}
+
+const Areas = () => {
+ const areas: Area[] = [
+ {
+ name: 'Аллаиховский ГУП "ЖКХ РС (Я)"',
+ svgPath: "M336 115L335 110L342 100L341 96L377 93L375 99L389 99L405 109L406 115L411 125L410 130L407 134L397 143L392 143L391 147L388 151L384 152L382 157L376 155L370 155L362 159L354 157L348 159L345 162L341 158L333 155L333 137L336 133L336 129L332 122L335 119z",
+ href: '/аллаиховский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Нижнеколымский филиал ГУП "ЖКХ РС(Я)"',
+ svgPath: "M404 108L412 125L411 129L414 132L421 134L427 131L435 133L445 130L453 133L464 128L467 128L474 133L485 123L488 119L488 116L493 115L495 113L494 106L491 105L492 102L488 100L487 96L484 95L479 90L469 98L459 98L456 89L453 85L439 83L427 89L410 104z",
+ href: '/нижнеколымский-филиал-гуп-жкх-рся'
+ },
+ {
+ name: 'Среднеколымский ГУП "ЖКХ РС (Я)"',
+ svgPath: "M410 129L406 135L395 143L397 155L401 158L399 162L394 165L394 168L396 172L400 173L406 179L428 181L436 186L444 188L446 192L468 196L466 186L481 182L483 179L486 178L491 178L492 174L496 170L491 157L488 153L479 152L474 148L474 144L476 141L473 139L473 132L469 129L464 127L456 130L453 133L444 131L434 134L425 131L421 134L415 133z",
+ href: '/среднеколымский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Верхнеколымский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M406 179L405 187L410 192L413 198L409 209L412 216L424 221L429 225L431 237L433 241L436 243L444 241L444 237L447 237L454 243L458 235L462 233L464 229L468 231L468 222L465 219L464 214L467 205L464 202L465 196L454 192L450 192L445 190L443 187L435 186L431 182L425 180L413 180z",
+ href: '/верхнеколымский-филиал-гуп-жкх-рся'
+ },
+ {
+ name: 'Момский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M405 187L414 200L410 206L412 217L425 222L429 228L436 256L431 253L429 257L417 257L411 255L405 256L397 249L389 251L383 245L379 247L372 245L369 247L359 247L355 244L354 240L349 233L351 229L350 226L337 219L333 216L331 212L331 209L340 210L344 208L347 206L352 198L357 198L362 203L384 210L390 198L397 196L398 190L401 189z",
+ href: '/момский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Верхоянский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M331 203L332 215L336 219L352 227L350 236L353 238L354 243L349 241L334 246L324 243L317 248L314 254L310 257L306 267L300 267L296 269L295 273L299 280L298 284L293 284L289 281L281 264L265 251L264 247L263 241L264 237L267 235L267 228L275 223L277 215L284 208L284 200L277 194L274 195L274 182L276 178L288 168L292 172L297 168L302 168L307 183L317 183L319 187L325 192L329 200z",
+ href: '/верхоянский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Томпонский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M357 248L353 256L354 262L358 267L365 269L365 274L370 274L372 286L368 296L370 307L373 310L373 314L364 316L361 319L358 324L357 334L353 337L338 338L337 342L334 339L334 334L331 329L325 325L319 324L316 320L314 311L301 311L300 308L296 306L290 306L300 299L300 287L298 284L298 279L295 274L296 270L299 268L304 268L307 265L309 258L314 254L320 245L326 243L330 246L333 246A344.01,344.01,0,0,0,344,242C349,242,357,245,357,248Z",
+ href: '/томпонский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Таттинский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M301 311L313 310L318 324L328 325L329 328L333 330L334 339L337 342L332 343L329 346L329 350L327 344L324 342L311 341L301 330L302 315z",
+ href: '/таттинский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Чурапчинский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M301 330L311 341L323 341L326 343L329 347L328 351L325 354L316 356L311 351L303 350L301 352L297 350L292 337L298 333z",
+ href: '/чурапчинский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Амгинский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M304 350L309 350L313 352L317 369L311 374L310 378L307 381L307 384L304 386L300 386L293 393L280 393L276 389L274 384L274 370L277 370L281 373L285 373L288 371L290 369L289 360L291 356L295 353L299 353z",
+ href: '/амгинский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Хангаласский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M286 363L288 372L281 373L279 370L261 369L241 376L239 379L240 382L237 382L236 379L231 378L227 374L225 367L229 369L233 367L234 362L243 360L250 356L254 356L256 354L256 351L259 349L276 351L278 358L282 359z",
+ href: '/хангаласский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Олекминский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M264 368L263 375L258 378L253 385L247 387L243 385L243 389L238 389L233 396L227 397L223 404L217 406L210 413L209 418L211 423L211 435L217 435L214 441L211 443L212 450L209 455L207 463L203 463L194 456L182 454L173 459L173 449L169 450L168 432L166 429L164 419L165 410L161 406L158 406L156 410L148 406L149 402L144 390L151 378L160 373L161 368L167 366L168 363A176.01,176.01,0,0,0,176,356C193,360,216,357,217,362C224,365,230,378,240,383C238,379,256,370,261,369Z",
+ href: '/олекминский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Сунтарский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M151 378L158 376L161 372L161 369L167 366L171 360L183 351L181 346L168 336L159 334L155 329L150 329L144 326L134 315L128 305L119 306L119 319L123 319L123 325L116 324L116 328L119 330L118 335L124 342L124 355L127 358L136 357L137 360L142 361L150 373z",
+ href: '/сунтарский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Нюрбинский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M175 316L171 326L172 337L160 333L157 334L156 330L141 325L128 305L124 304L123 293L128 291L130 287L148 287L149 282L156 276L160 270L164 270L164 275L171 292L170 297L172 311L175 312z",
+ href: '/нюрбинский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Оленекский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M123 293L126 290L129 290L131 287L137 285L143 285L148 287L149 281L153 279L161 270L161 263L163 259L166 257L170 257L171 252L174 248L168 243L158 243L158 240L162 236L159 230L164 229L169 220L175 219L177 215L181 213L175 199L181 183L181 171L188 171L191 155L191 150L183 131L157 133L151 139L151 143L147 142L140 145L140 149L145 154L136 160L131 159L130 156L133 150L130 138L131 134L127 130L124 131L124 152L121 155L110 159L107 165L102 165L99 167L99 177L96 179L93 177L72 179L74 195L73 216L69 219L67 228L63 229L58 233L59 246L65 243L66 247L73 247L75 243L78 241L83 245L83 259L87 257L95 249L103 249L105 247L114 250L120 257L119 267L114 268L112 273L109 274L111 278L110 281L119 291z",
+ href: '/оленекский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Анабарский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M129 159L136 160L140 158L144 154L141 151L140 147L142 144L146 142L151 142L150 139L158 133L182 132L179 123L168 120L166 117L161 116L161 112L172 108L171 104L164 105L155 100L151 100L139 103L137 108L135 104L137 100L136 96L129 97L123 94L121 98L116 102L120 127L131 135L131 144L133 149L132 153z",
+ href: '/анабарский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Булунский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M169 108L169 112L164 110L161 112L163 115L160 115L165 116L170 122L179 123L189 143L191 152L189 170L181 173L179 188L175 198L187 193L195 199L203 199L209 204L213 204L216 201L220 201L222 204L232 206L235 203L246 202L254 198L257 193L267 193L270 198L274 195L273 183L280 174L277 171L275 165L268 162L265 152L262 152L259 157L258 154L251 154L247 151L242 141L238 137L242 133L244 127L236 123L240 113L235 106L228 102L222 103L220 106L207 98L198 106L202 112L201 119L198 115L193 113L193 117L190 118L179 111L176 112L174 115L171 114z",
+ href: '/булунский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Эвено-Бытантайский ГУП "ЖКХ РС (Я)"',
+ svgPath: "M269 196L271 198L275 195L279 195L283 198L285 202L282 212L279 213L276 217L276 221L273 226L269 228L267 235L263 239L266 252L262 250L249 248L243 249L241 244L241 234L234 227L232 219L235 210L231 206L235 203L245 202L248 199L252 199L258 193L267 193z",
+ href: '/эвено-бытантайский-филиал-гуп-жкх-рс'
+ },
+ {
+ name: 'Жиганский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M232 206L235 211L233 221L234 226L241 235L242 249L239 251L244 267L241 274L225 281L222 288L210 293L202 291L202 287L193 285L187 275L182 272L182 258L178 254L178 250L170 246L168 243L158 242L161 234L157 230L165 229L168 220L172 221L176 214L181 213L181 209L176 202L176 199L187 193L190 194L196 200L199 198L205 199L206 203L210 204L218 201L221 204L227 206z",
+ href: '/жиганский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Вилюйский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M174 248L178 251L182 260L182 273L185 275L191 284L202 288L203 291L212 293L222 288L223 292L227 297L224 298L213 317L218 323L227 325L232 329L234 333L226 335L224 338L211 338L206 341L202 341L202 346L199 344L197 325L191 317L194 311L192 304L189 306L184 306L184 302L178 295L177 286L174 284L173 277L176 264L173 263L170 256L172 251z",
+ href: '/вилюйский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Верхневилюйский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M169 257L176 263L172 279L178 288L179 297L184 305L193 305L192 316L193 320L197 323L198 344L202 345L207 351L206 359L194 360L176 356L184 350L178 341L172 337L172 326L176 315L172 309L169 286L166 282L162 265L164 257z",
+ href: '/верхневилюйский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Горный ГУП "ЖКХ РС(Я)"',
+ svgPath: "M202 340L202 345L208 353L206 359L215 357L219 363L229 369L233 368L234 361L241 361L249 357L254 357L258 350L266 347L265 344L260 342L259 336L262 333L261 330L259 329L255 320L252 319L249 321L248 325L241 327L236 326L234 329L232 329L234 333L227 334L224 338L215 336L207 339z",
+ href: '/горный-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Кобяйский ГУП "ЖКХ РС (Я)"',
+ svgPath: "M217 323L214 313L224 299L227 298L222 288L223 284L232 277L240 274L244 268L244 263L239 256L239 251L243 249L260 250L273 257L281 263L283 270L291 283L295 285L298 283L300 291L299 300L288 308L278 309L275 315L271 315L269 312L266 314L261 324L258 324L255 319L248 321L247 326L238 326L237 329L232 329L230 326L222 324z",
+ href: '/кобяйский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Мегино-кангаласский ГУП "ЖКХ РС(Я)"',
+ svgPath: "M287 371L289 369L291 356L300 351L296 348L294 340L285 334L280 339L282 342L280 349L277 353L278 358L287 362L287 367z",
+ href: '/мегино-кангаласский-филиал-гуп-жкх-рс'
+ },
+ {
+ name: 'г. Якутск',
+ svgPath: "M259 341L261 339L275 337L281 340L277 352L268 350L268 347L264 343z",
+ href: '/rukovodstvo'
+ },
+ {
+ name: 'Абыйский ГУП "ЖКХ РС (Я)"',
+ svgPath: "M396 144L396 151L398 156L401 157L400 161L393 166L396 171L406 180L407 187L404 190L398 189L397 198L392 198L389 201L386 212L372 208L365 204L361 204L353 198L351 200L344 189L346 184L344 168L347 160L351 157L356 156L363 160L367 158L369 154L374 154L382 158L385 150L389 149L392 145z",
+ href: '/абыйский-филиал-гуп-жкх-рс-я'
+ },
+ {
+ name: 'Усть-Алданский ГУП "ЖКХ РС (Я)"',
+ svgPath: "M284 335L294 335L301 331L302 310L291 305L288 308L277 309L275 314L277 319L276 325L279 329L280 334L278 336L280 339L283 338z",
+ href: '/усть-алданский-филиал-гуп-жкх-рс-я'
+ }
+ ]
+
+ const [tooltip, setTooltip] = useState<{ name: string, x: number, y: number } | null>(null)
+
+ return (
+
+ )
+}
+
+export default Areas
\ No newline at end of file
diff --git a/src/components/WPRenderer/SmartSlider.tsx b/src/components/WPRenderer/SmartSlider.tsx
index efb7117..6e971bb 100644
--- a/src/components/WPRenderer/SmartSlider.tsx
+++ b/src/components/WPRenderer/SmartSlider.tsx
@@ -1,34 +1,179 @@
+'use client'
+
import { montserratFont } from "@/app/layout";
+import { CarouselSlide } from "@/types/entities";
+
+interface SmartSliderProps {
+ sliderId: string
+ slidesPerView?: number
+ slides: CarouselSlide[]
+ showIndicator?: boolean
+ showTitle?: boolean
+ padding?: boolean
+}
+
+export const SmartSlider = ({ sliderId, padding, showTitle, slidesPerView, slides, showIndicator }: SmartSliderProps) => {
+ // return (
+ // <>
+ //
+ //
+ //
+ //
+ //
+ // Page 1
+ //
+ //
+ // Page 2
+ //
+ //
+ // Page 3
+ //
+ //
+ // Page 4
+ //
+ //
+ //
+ //
+ // >
+ // )
-export const SmartSlider = ({ sliderId }: { sliderId: string }) => {
return (
-
+
-
+ {showIndicator &&
+
+ {slides.map((slide) => (
+
+
+ ))}
+
}
);
};
\ No newline at end of file
diff --git a/src/components/WPRenderer/WPRenderer.tsx b/src/components/WPRenderer/WPRenderer.tsx
index a7f01f0..c2af828 100644
--- a/src/components/WPRenderer/WPRenderer.tsx
+++ b/src/components/WPRenderer/WPRenderer.tsx
@@ -1,7 +1,9 @@
import parse, { HTMLReactParserOptions } from 'html-react-parser';
import { SmartSlider } from './SmartSlider';
import PostGrid from '../WP/PostGrid/PostGrid.server';
-import { PagePayload } from '@/types/entities';
+import { CarouselSlide, PagePayload } from '@/types/entities';
+import { transformLinks } from '@/lib/utils';
+import EmblaCarousel from '../UI/EmblaCarousel/EmblaCarousel';
const options: HTMLReactParserOptions = {
replace: (domNode) => {
@@ -36,14 +38,14 @@ const replaceShortcodes = (html: string) => {
);
};
-const transformLinks = (html: string) => {
- // Regex to match https://specificdomain.com/?page_id= followed by numbers
- // Using a more specific pattern to avoid matching other domains
- const regex = /(https?:\/\/)?(www\.)?new.jkhsakha\.ru\/(?!wp-content)(\/[^\s"']*)/g
+// const transformLinks = (html: string) => {
+// // Regex to match https://specificdomain.com/?page_id= followed by numbers
+// // Using a more specific pattern to avoid matching other domains
+// const regex = /(https?:\/\/)?(www\.)?new.jkhsakha\.ru\/(?!wp-content)(\/[^\s"']*)/g
- // Replace only the specific domain links with relative paths
- return html.replace(regex, '$3');
-};
+// // Replace only the specific domain links with relative paths
+// return html.replace(regex, '$3');
+// };
export const renderPostContent = (content: string, payload?: PagePayload) => {
@@ -52,6 +54,26 @@ export const renderPostContent = (content: string, payload?: PagePayload) => {
transformLinks(content)
)
+ const slides: CarouselSlide[] = [
+ {
+ id: 0,
+ title: "Тепло сердец - людям!",
+ src: "/first_slide2.jpg",
+ },
+ {
+ id: 1,
+ title: "Личный кабинет",
+ src: "/Личный-кабинет.jpg",
+ href: "https://lk.jkhsakha.ru",
+ },
+ {
+ id: 2,
+ title: "Способы оплаты",
+ src: "/Способ-оплаты.jpg",
+ href: "/sposoby-oplaty/",
+ }
+ ]
+
return (
{parse(transformedContent, {
@@ -60,8 +82,12 @@ export const renderPostContent = (content: string, payload?: PagePayload) => {
domNode &&
domNode.type === 'tag' && domNode.attribs?.['data-shortcode'] === 'smartslider3'
) {
+ return (
+
+ )
return (
);
@@ -72,7 +98,7 @@ export const renderPostContent = (content: string, payload?: PagePayload) => {
const gridId = Number(domNode.attribs["data-grid-id"]);
if (!gridId) return null;
return (
-
+
);
} else if (
domNode &&
diff --git a/src/components/WebVitalsCard.tsx b/src/components/WebVitalsCard.tsx
new file mode 100644
index 0000000..1c9e7ce
--- /dev/null
+++ b/src/components/WebVitalsCard.tsx
@@ -0,0 +1,75 @@
+// components/WebVitalsCard.tsx
+'use client'
+
+interface WebVitalsCardProps {
+ title: string
+ value: number | null
+ unit: string
+ description: string
+ thresholds: {
+ good: number
+ needsImprovement: number
+ }
+}
+
+export function WebVitalsCard({ title, value, unit, description, thresholds }: WebVitalsCardProps) {
+ const getStatusColor = () => {
+ if (value === null) return 'bg-gray-100 border-gray-200'
+ if (value <= thresholds.good) return 'bg-green-50 border-green-200'
+ if (value <= thresholds.needsImprovement) return 'bg-yellow-50 border-yellow-200'
+ return 'bg-red-50 border-red-200'
+ }
+
+ const getStatusText = () => {
+ if (value === null) return 'Not measured'
+ if (value <= thresholds.good) return 'Good'
+ if (value <= thresholds.needsImprovement) return 'Needs Improvement'
+ return 'Poor'
+ }
+
+ const getStatusColorBadge = () => {
+ if (value === null) return 'bg-gray-200 text-gray-700'
+ if (value <= thresholds.good) return 'bg-green-200 text-green-800'
+ if (value <= thresholds.needsImprovement) return 'bg-yellow-200 text-yellow-800'
+ return 'bg-red-200 text-red-800'
+ }
+
+ const formattedValue = value !== null ? value.toFixed(2) : '—'
+
+ return (
+
+
+
{title}
+
+ {getStatusText()}
+
+
+
+
+ {formattedValue}
+ {unit}
+
+
+
{description}
+
+
+
+ Good
+ Needs Improvement
+ Poor
+
+
+
+ ≤ {thresholds.good}{unit}
+ ≤ {thresholds.needsImprovement}{unit}
+ > {thresholds.needsImprovement}{unit}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/hooks/useWebVitals.ts b/src/hooks/useWebVitals.ts
new file mode 100644
index 0000000..226f010
--- /dev/null
+++ b/src/hooks/useWebVitals.ts
@@ -0,0 +1,89 @@
+// hooks/useWebVitals.ts
+'use client'
+
+import { useEffect, useState } from 'react'
+
+interface WebVitalsMetrics {
+ FCP: number | null
+ LCP: number | null
+ FID: number | null
+ CLS: number | null
+ TTFB: number | null
+ INP: number | null
+}
+
+export function useWebVitals() {
+ const [metrics, setMetrics] = useState({
+ FCP: null,
+ LCP: null,
+ FID: null,
+ CLS: null,
+ TTFB: null,
+ INP: null,
+ })
+
+ useEffect(() => {
+ if (typeof window === 'undefined' || !('performance' in window)) return
+
+ // Create a PerformanceObserver to capture metrics
+ const observer = new PerformanceObserver((list) => {
+ list.getEntries().forEach((entry) => {
+ // First Contentful Paint
+ if (entry.entryType === 'paint' && entry.name === 'first-contentful-paint') {
+ setMetrics(prev => ({ ...prev, FCP: entry.startTime }))
+ }
+
+ // Largest Contentful Paint
+ if (entry.entryType === 'largest-contentful-paint') {
+ setMetrics(prev => ({ ...prev, LCP: entry.startTime }))
+ }
+
+ // First Input Delay
+ if (entry.entryType === 'first-input') {
+ const fidEntry = entry as PerformanceEventTiming
+ setMetrics(prev => ({ ...prev, FID: fidEntry.processingStart - fidEntry.startTime }))
+ }
+
+ // Cumulative Layout Shift
+ if (entry.entryType === 'layout-shift') {
+ const clsEntry = entry as any // LayoutShift
+ if (!clsEntry.hadRecentInput) {
+ setMetrics(prev => ({
+ ...prev,
+ CLS: (prev.CLS || 0) + clsEntry.value
+ }))
+ }
+ }
+
+ // Interaction to Next Paint
+ if (entry.entryType === 'event' || entry.entryType === 'first-input') {
+ const inpEntry = entry as any // PerformanceEventTiming
+ if (inpEntry.interactionId) {
+ setMetrics(prev => ({
+ ...prev,
+ INP: Math.max(prev.INP || 0, inpEntry.duration)
+ }))
+ }
+ }
+ })
+ })
+
+ // TTFB can be obtained from navigation timing
+ const navigationEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
+ if (navigationEntry) {
+ setMetrics(prev => ({
+ ...prev,
+ TTFB: navigationEntry.responseStart - navigationEntry.requestStart
+ }))
+ }
+
+ // Observe different metric types
+ observer.observe({
+ entryTypes: ['paint', 'largest-contentful-paint', 'first-input', 'layout-shift', 'event']
+ })
+
+ return () => observer.disconnect()
+ }, [])
+
+ return metrics
+}
\ No newline at end of file
diff --git a/src/lib/transliterate.ts b/src/lib/transliterate.ts
new file mode 100644
index 0000000..d3d0b6c
--- /dev/null
+++ b/src/lib/transliterate.ts
@@ -0,0 +1,99 @@
+// utils/transliterate.ts
+
+// Define the type for the transliteration map
+type TransliterationMap = {
+ [key: string]: string;
+};
+
+export const transliterationMap: TransliterationMap = {
+ // Russian/Ukrainian/Bulgarian Cyrillic
+ 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd',
+ 'е': 'e', 'ё': 'jo', 'ж': 'zh', 'з': 'z', 'и': 'i',
+ 'й': 'j', // 'й' -> 'j'
+ 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n',
+ 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't',
+ 'у': 'u', 'ф': 'f', 'х': 'h', // 'х' -> 'h' (not 'kh')
+ 'ц': 'c', // 'ц' -> 'c' (not 'ts')
+ 'ч': 'ch', 'ш': 'sh', 'щ': 'shch',
+ 'ъ': '', 'ы': 'y', 'ь': '', // Soft sign becomes empty
+ 'э': 'je', // 'э' -> 'je' (crucial for your example)
+ 'ю': 'ju', // 'ю' -> 'ju'
+ 'я': 'ja', // 'я' -> 'ja'
+
+ // Capital letters
+ 'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D',
+ 'Е': 'E', 'Ё': 'Jo', 'Ж': 'Zh', 'З': 'Z', 'И': 'I',
+ 'Й': 'J', 'К': 'K', 'Л': 'L', 'М': 'M', 'Н': 'N',
+ 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T',
+ 'У': 'U', 'Ф': 'F', 'Х': 'H', 'Ц': 'C',
+ 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Shch',
+ 'Ъ': '', 'Ы': 'Y', 'Ь': '',
+ 'Э': 'Je', // 'Э' -> 'Je'
+ 'Ю': 'Ju', 'Я': 'Ja'
+};
+
+
+/**
+ * Transliterates Cyrillic text to Latin characters
+ * @param text - The input text to transliterate
+ * @returns Transliterated text with Latin characters
+ */
+export function transliterate(text: string): string {
+ if (!text) return '';
+
+ return text.split('').map((char: string): string => {
+ return transliterationMap[char] || char;
+ }).join('');
+}
+
+// Optional: Add a more comprehensive version with options
+interface TransliterateOptions {
+ preserveCase?: boolean;
+ ignoreUnknown?: boolean;
+}
+
+/**
+ * Advanced transliteration with options
+ * @param text - The input text to transliterate
+ * @param options - Configuration options
+ * @returns Transliterated text
+ */
+export function transliterateAdvanced(
+ text: string,
+ options: TransliterateOptions = {}
+): string {
+ if (!text) return '';
+
+ const { preserveCase = false, ignoreUnknown = true } = options;
+
+ return text.split('').map((char: string): string => {
+ const transliterated = transliterationMap[char];
+
+ if (transliterated !== undefined) {
+ return transliterated;
+ }
+
+ // Handle unknown characters
+ if (!ignoreUnknown) {
+ // You might want to log or handle unknown characters differently
+ console.warn(`Unknown character found: ${char}`);
+ }
+
+ return char;
+ }).join('');
+}
+
+export const containsCyrillic = (text: string): boolean => {
+ // First, decode the URL-encoded string
+ try {
+ const decodedText = decodeURIComponent(text);
+
+ const cyrillicPattern = /[а-яА-ЯёЁіІїЇєЄґҐ]/;
+ return cyrillicPattern.test(decodedText);
+ } catch (e) {
+ // If decoding fails, test the original text
+ console.error('Error decoding URL:', e);
+ const cyrillicPattern = /[а-яА-ЯёЁіІїЇєЄґҐ]/;
+ return cyrillicPattern.test(text);
+ }
+};
\ No newline at end of file
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index e8fdd71..8e0ca8d 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,12 +1,51 @@
-export const transformLinks = (html: string) => {
- // Regex to match https://specificdomain.com/?page_id= followed by numbers
- // Using a more specific pattern to avoid matching other domains
- const regex = /(https?:\/\/)?(www\.)?new.jkhsakha\.ru\/(?!wp-content)(\/[^\s"']*)/g
+import { containsCyrillic, transliterate } from "./transliterate";
- // Replace only the specific domain links with relative paths
- return html.replace(regex, '$3');
+export const transformLinks = (html: string): string => {
+ if (!html) return html;
+
+ // Regex to match specific domain links, excluding wp-content
+ // Now also captures the full URL to handle different parts
+ const regex = /(https?:\/\/)?(www\.)?new\.jkhsakha\.ru\/(?!wp-content)([^\s"']*)/g;
+
+ return html.replace(regex, (match, protocol, www, path) => {
+ // Extract the full path including leading slash
+ let fullPath = path;
+
+ // Make sure path starts with /
+ if (!fullPath.startsWith('/')) {
+ fullPath = '/' + fullPath;
+ }
+
+ fullPath = decodeURIComponent(fullPath)
+
+ // Check if the path contains Cyrillic characters
+ if (containsCyrillic(fullPath)) {
+ console.log("contains Cyrillic, transliterating:", fullPath);
+ // Split the path into segments
+ const pathSegments = fullPath.split('/').filter((segment: any) => segment.length > 0);
+
+ // Transliterate each segment that contains Cyrillic
+ const transliteratedSegments = pathSegments.map((segment: any) => {
+ return containsCyrillic(segment) ? transliterate(segment) : segment;
+ });
+
+ // Reconstruct the path
+ const transliteratedPath = '/' + transliteratedSegments.join('/');
+
+ // Ensure the path ends with trailing slash if original had it
+ if (fullPath.endsWith('/') && !transliteratedPath.endsWith('/')) {
+ return transliteratedPath + '/';
+ }
+
+ return transliteratedPath;
+ }
+
+ // If no Cyrillic, return the original path
+ return fullPath;
+ });
};
+
export function toRelativePath(input: string): string {
try {
// Handles absolute URLs
@@ -17,4 +56,14 @@ export function toRelativePath(input: string): string {
if (input.startsWith('/')) return input;
return '/' + input;
}
-}
\ No newline at end of file
+}
+
+const extractPathFromUrl = (url: string): string => {
+ try {
+ const urlObject = new URL(url);
+ return urlObject.pathname;
+ } catch {
+ // If URL parsing fails, return the original string
+ return url;
+ }
+};
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 47385d8..256e2a3 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -10,4 +10,98 @@
h1 {
@apply text-3xl;
/* Or any size you prefer */
+}
+
+.tiptap {
+ img {
+ margin: 0;
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+ }
+
+ [data-resize-handle] {
+ position: absolute;
+ background: rgba(0, 0, 0, 0.5);
+ border: 1px solid rgba(255, 255, 255, 0.8);
+ border-radius: 2px;
+ z-index: 10;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.8);
+ }
+
+ /* Corner handles */
+ &[data-resize-handle='top-left'],
+ &[data-resize-handle='top-right'],
+ &[data-resize-handle='bottom-left'],
+ &[data-resize-handle='bottom-right'] {
+ width: 8px;
+ height: 8px;
+ }
+
+ &[data-resize-handle='top-left'] {
+ top: -4px;
+ left: -4px;
+ cursor: nwse-resize;
+ }
+
+ &[data-resize-handle='top-right'] {
+ top: -4px;
+ right: -4px;
+ cursor: nesw-resize;
+ }
+
+ &[data-resize-handle='bottom-left'] {
+ bottom: -4px;
+ left: -4px;
+ cursor: nesw-resize;
+ }
+
+ &[data-resize-handle='bottom-right'] {
+ bottom: -4px;
+ right: -4px;
+ cursor: nwse-resize;
+ }
+
+ /* Edge handles */
+ &[data-resize-handle='top'],
+ &[data-resize-handle='bottom'] {
+ height: 6px;
+ left: 8px;
+ right: 8px;
+ }
+
+ &[data-resize-handle='top'] {
+ top: -3px;
+ cursor: ns-resize;
+ }
+
+ &[data-resize-handle='bottom'] {
+ bottom: -3px;
+ cursor: ns-resize;
+ }
+
+ &[data-resize-handle='left'],
+ &[data-resize-handle='right'] {
+ width: 6px;
+ top: 8px;
+ bottom: 8px;
+ }
+
+ &[data-resize-handle='left'] {
+ left: -3px;
+ cursor: ew-resize;
+ }
+
+ &[data-resize-handle='right'] {
+ right: -3px;
+ cursor: ew-resize;
+ }
+ }
+
+ [data-resize-state='true'] [data-resize-wrapper] {
+ outline: 1px solid rgba(0, 0, 0, 0.25);
+ border-radius: 0.125rem;
+ }
}
\ No newline at end of file
diff --git a/src/types/elements.ts b/src/types/elements.ts
index ec1ac83..b14eae1 100644
--- a/src/types/elements.ts
+++ b/src/types/elements.ts
@@ -1,8 +1,10 @@
+import CookieNotice from "@/components/CookieNotice/CookieNotice";
+
export interface ContentScheme {
content: ElementTypes[]
}
-export type ElementTypes = Banner | Center | Navbar | BannerCompact
+export type ElementTypes = Banner | Center | Navbar | BannerCompact | CookieNotice
export type ElementBase = {
order: number
diff --git a/src/types/entities.ts b/src/types/entities.ts
index 3e27907..1135136 100644
--- a/src/types/entities.ts
+++ b/src/types/entities.ts
@@ -3,6 +3,7 @@ export interface PostData {
post_author: string;
post_name: string;
post_title: string;
+ post_type: string;
post_excerpt: string;
post_date: string;
post_date_gmt: string;
@@ -12,4 +13,12 @@ export interface PostData {
export interface PagePayload {
page: number;
+}
+
+export interface CarouselSlide {
+ id: number
+ title?: string
+ description?: string
+ src: string
+ href?: string
}
\ No newline at end of file