create-pxlr 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -0
- package/package.json +51 -0
- package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
- package/templates/blog/frontend/app/blog/page.tsx +102 -0
- package/templates/blog/frontend/app/components/footer.tsx +21 -0
- package/templates/blog/frontend/app/components/header.tsx +45 -0
- package/templates/blog/frontend/app/globals.css +30 -0
- package/templates/blog/frontend/app/layout.tsx +38 -0
- package/templates/blog/frontend/app/lib/cms.ts +71 -0
- package/templates/blog/frontend/app/page.tsx +155 -0
- package/templates/blog/frontend/next.config.ts +16 -0
- package/templates/blog/frontend/package.json +24 -0
- package/templates/blog/frontend/postcss.config.mjs +7 -0
- package/templates/blog/frontend/tsconfig.json +23 -0
- package/templates/blog/pxlr-cms/README.md +188 -0
- package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
- package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
- package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
- package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
- package/templates/clean/pxlr-cms/README.md +188 -0
- package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
- package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
- package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/clean/pxlr-cms/packages/shared/tsconfig.json +18 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { getBlogPosts } from '@/app/lib/cms';
|
|
3
|
+
|
|
4
|
+
export const revalidate = 60;
|
|
5
|
+
|
|
6
|
+
export default async function BlogPage() {
|
|
7
|
+
const posts = await getBlogPosts();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
11
|
+
<div className="mb-12">
|
|
12
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-4">Блог</h1>
|
|
13
|
+
<p className="text-xl text-gray-600">
|
|
14
|
+
Последние новости, статьи и обновления
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{posts.length === 0 ? (
|
|
19
|
+
<div className="text-center py-12">
|
|
20
|
+
<div className="text-gray-400 text-6xl mb-4">📝</div>
|
|
21
|
+
<h2 className="text-xl font-semibold text-gray-600 mb-2">
|
|
22
|
+
Записей пока нет
|
|
23
|
+
</h2>
|
|
24
|
+
<p className="text-gray-500">
|
|
25
|
+
Скоро здесь появятся интересные статьи
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
) : (
|
|
29
|
+
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
30
|
+
{posts.map((post) => (
|
|
31
|
+
<article
|
|
32
|
+
key={post.id}
|
|
33
|
+
className="bg-white rounded-2xl border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow"
|
|
34
|
+
>
|
|
35
|
+
{post.data.image?.url ? (
|
|
36
|
+
<div className="aspect-video bg-gray-100">
|
|
37
|
+
<img
|
|
38
|
+
src={post.data.image.url}
|
|
39
|
+
alt={post.data.image.alt || post.data.title}
|
|
40
|
+
className="w-full h-full object-cover"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
) : (
|
|
44
|
+
<div className="aspect-video bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center">
|
|
45
|
+
<span className="text-4xl">📄</span>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
<div className="p-6">
|
|
49
|
+
<div className="flex items-center gap-2 text-sm text-gray-500 mb-3">
|
|
50
|
+
{post.data.publishedAt && (
|
|
51
|
+
<time dateTime={post.data.publishedAt}>
|
|
52
|
+
{new Date(post.data.publishedAt).toLocaleDateString('ru-RU', {
|
|
53
|
+
year: 'numeric',
|
|
54
|
+
month: 'long',
|
|
55
|
+
day: 'numeric',
|
|
56
|
+
})}
|
|
57
|
+
</time>
|
|
58
|
+
)}
|
|
59
|
+
{post.data.author && (
|
|
60
|
+
<>
|
|
61
|
+
<span>•</span>
|
|
62
|
+
<span>{post.data.author}</span>
|
|
63
|
+
</>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<Link href={`/blog/${post.data.slug}`}>
|
|
67
|
+
<h2 className="text-xl font-semibold text-gray-900 mb-2 hover:text-blue-600 transition-colors">
|
|
68
|
+
{post.data.title}
|
|
69
|
+
</h2>
|
|
70
|
+
</Link>
|
|
71
|
+
{post.data.excerpt && (
|
|
72
|
+
<p className="text-gray-600 line-clamp-3">
|
|
73
|
+
{post.data.excerpt}
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
<Link
|
|
77
|
+
href={`/blog/${post.data.slug}`}
|
|
78
|
+
className="inline-flex items-center mt-4 text-blue-600 font-medium hover:text-blue-700"
|
|
79
|
+
>
|
|
80
|
+
Читать далее
|
|
81
|
+
<svg
|
|
82
|
+
className="ml-1 w-4 h-4"
|
|
83
|
+
fill="none"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
viewBox="0 0 24 24"
|
|
86
|
+
>
|
|
87
|
+
<path
|
|
88
|
+
strokeLinecap="round"
|
|
89
|
+
strokeLinejoin="round"
|
|
90
|
+
strokeWidth={2}
|
|
91
|
+
d="M9 5l7 7-7 7"
|
|
92
|
+
/>
|
|
93
|
+
</svg>
|
|
94
|
+
</Link>
|
|
95
|
+
</div>
|
|
96
|
+
</article>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function Footer() {
|
|
2
|
+
return (
|
|
3
|
+
<footer className="bg-gray-50 border-t border-gray-200 mt-auto">
|
|
4
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
5
|
+
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
|
6
|
+
<div className="flex items-center gap-2">
|
|
7
|
+
<div className="w-6 h-6 bg-black rounded flex items-center justify-center">
|
|
8
|
+
<span className="text-white font-bold text-xs">P</span>
|
|
9
|
+
</div>
|
|
10
|
+
<span className="text-gray-600 text-sm">
|
|
11
|
+
Создано с помощью PXLR CMS
|
|
12
|
+
</span>
|
|
13
|
+
</div>
|
|
14
|
+
<div className="text-gray-500 text-sm">
|
|
15
|
+
© {new Date().getFullYear()} Все права защищены
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</footer>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
const navigation = [
|
|
4
|
+
{ name: 'Главная', href: '/' },
|
|
5
|
+
{ name: 'Блог', href: '/blog' },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export function Header() {
|
|
9
|
+
return (
|
|
10
|
+
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
|
|
11
|
+
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
12
|
+
<div className="flex justify-between h-16">
|
|
13
|
+
<div className="flex items-center">
|
|
14
|
+
<Link href="/" className="flex items-center gap-2">
|
|
15
|
+
<div className="w-8 h-8 bg-black rounded-lg flex items-center justify-center">
|
|
16
|
+
<span className="text-white font-bold text-sm">P</span>
|
|
17
|
+
</div>
|
|
18
|
+
<span className="font-semibold text-xl">PXLR</span>
|
|
19
|
+
</Link>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div className="flex items-center space-x-8">
|
|
23
|
+
{navigation.map((item) => (
|
|
24
|
+
<Link
|
|
25
|
+
key={item.name}
|
|
26
|
+
href={item.href}
|
|
27
|
+
className="text-gray-600 hover:text-gray-900 font-medium transition-colors"
|
|
28
|
+
>
|
|
29
|
+
{item.name}
|
|
30
|
+
</Link>
|
|
31
|
+
))}
|
|
32
|
+
<a
|
|
33
|
+
href="http://localhost:3333"
|
|
34
|
+
target="_blank"
|
|
35
|
+
rel="noopener noreferrer"
|
|
36
|
+
className="bg-black text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-800 transition-colors"
|
|
37
|
+
>
|
|
38
|
+
Админка
|
|
39
|
+
</a>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</nav>
|
|
43
|
+
</header>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* Only scan app folder, exclude pxlr-cms */
|
|
4
|
+
@source "../app/**/*.{js,ts,jsx,tsx}";
|
|
5
|
+
@source "../components/**/*.{js,ts,jsx,tsx}";
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--background: #ffffff;
|
|
9
|
+
--foreground: #171717;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@theme inline {
|
|
13
|
+
--color-background: var(--background);
|
|
14
|
+
--color-foreground: var(--foreground);
|
|
15
|
+
--font-sans: var(--font-geist-sans);
|
|
16
|
+
--font-mono: var(--font-geist-mono);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (prefers-color-scheme: dark) {
|
|
20
|
+
:root {
|
|
21
|
+
--background: #0a0a0a;
|
|
22
|
+
--foreground: #ededed;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
background: var(--background);
|
|
28
|
+
color: var(--foreground);
|
|
29
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
30
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
import { Header } from "./components/header";
|
|
5
|
+
import { Footer } from "./components/footer";
|
|
6
|
+
|
|
7
|
+
const geistSans = Geist({
|
|
8
|
+
variable: "--font-geist-sans",
|
|
9
|
+
subsets: ["latin"],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const geistMono = Geist_Mono({
|
|
13
|
+
variable: "--font-geist-mono",
|
|
14
|
+
subsets: ["latin"],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const metadata: Metadata = {
|
|
18
|
+
title: "PXLR Site",
|
|
19
|
+
description: "Сайт на базе PXLR CMS",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default function RootLayout({
|
|
23
|
+
children,
|
|
24
|
+
}: Readonly<{
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
}>) {
|
|
27
|
+
return (
|
|
28
|
+
<html lang="ru">
|
|
29
|
+
<body
|
|
30
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`}
|
|
31
|
+
>
|
|
32
|
+
<Header />
|
|
33
|
+
<main className="flex-1">{children}</main>
|
|
34
|
+
<Footer />
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const API_URL = process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:4000';
|
|
2
|
+
|
|
3
|
+
export interface BlogPost {
|
|
4
|
+
id: string;
|
|
5
|
+
schema_name: string;
|
|
6
|
+
data: {
|
|
7
|
+
title: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
excerpt?: string;
|
|
10
|
+
content?: string;
|
|
11
|
+
image?: { url: string; alt?: string };
|
|
12
|
+
publishedAt?: string;
|
|
13
|
+
author?: string;
|
|
14
|
+
};
|
|
15
|
+
created_at: string;
|
|
16
|
+
updated_at: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ApiResponse<T> {
|
|
20
|
+
documents?: T[];
|
|
21
|
+
document?: T;
|
|
22
|
+
pagination?: {
|
|
23
|
+
page: number;
|
|
24
|
+
limit: number;
|
|
25
|
+
total: number;
|
|
26
|
+
totalPages: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Получить все записи блога (только опубликованные)
|
|
31
|
+
export async function getBlogPosts(): Promise<BlogPost[]> {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`${API_URL}/content?schema=blog&status=published&limit=50`, {
|
|
34
|
+
next: { revalidate: 60 },
|
|
35
|
+
});
|
|
36
|
+
const data: ApiResponse<BlogPost> = await res.json();
|
|
37
|
+
return data.documents || [];
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Failed to fetch blog posts:', error);
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Получить пост по slug (только опубликованные)
|
|
45
|
+
export async function getBlogPostBySlug(slug: string): Promise<BlogPost | null> {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${API_URL}/content?schema=blog&status=published&limit=50`, {
|
|
48
|
+
next: { revalidate: 60 },
|
|
49
|
+
});
|
|
50
|
+
const data: ApiResponse<BlogPost> = await res.json();
|
|
51
|
+
const post = data.documents?.find((p) => p.data.slug === slug);
|
|
52
|
+
return post || null;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Failed to fetch blog post:', error);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Получить пост по ID
|
|
60
|
+
export async function getBlogPostById(id: string): Promise<BlogPost | null> {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`${API_URL}/content/${id}`, {
|
|
63
|
+
next: { revalidate: 60 },
|
|
64
|
+
});
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
return data.document || null;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Failed to fetch blog post:', error);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { getBlogPosts } from '@/app/lib/cms';
|
|
3
|
+
|
|
4
|
+
export const revalidate = 60;
|
|
5
|
+
|
|
6
|
+
export default async function HomePage() {
|
|
7
|
+
const posts = await getBlogPosts();
|
|
8
|
+
const latestPosts = posts.slice(0, 3);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
{/* Hero Section */}
|
|
13
|
+
<section className="bg-gradient-to-br from-gray-900 to-gray-800 text-white">
|
|
14
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
|
15
|
+
<div className="max-w-3xl">
|
|
16
|
+
<h1 className="text-5xl md:text-6xl font-bold mb-6">
|
|
17
|
+
Добро пожаловать в{' '}
|
|
18
|
+
<span className="text-blue-400">PXLR</span>
|
|
19
|
+
</h1>
|
|
20
|
+
<p className="text-xl text-gray-300 mb-8">
|
|
21
|
+
Современный сайт, созданный с помощью собственной CMS.
|
|
22
|
+
Полный контроль над контентом, без зависимости от облачных сервисов.
|
|
23
|
+
</p>
|
|
24
|
+
<div className="flex flex-wrap gap-4">
|
|
25
|
+
<Link
|
|
26
|
+
href="/blog"
|
|
27
|
+
className="bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
|
28
|
+
>
|
|
29
|
+
Читать блог
|
|
30
|
+
</Link>
|
|
31
|
+
<a
|
|
32
|
+
href="http://localhost:3333"
|
|
33
|
+
target="_blank"
|
|
34
|
+
rel="noopener noreferrer"
|
|
35
|
+
className="bg-white/10 hover:bg-white/20 text-white px-6 py-3 rounded-lg font-medium transition-colors border border-white/20"
|
|
36
|
+
>
|
|
37
|
+
Открыть админку
|
|
38
|
+
</a>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
|
|
44
|
+
{/* Features Section */}
|
|
45
|
+
<section className="py-20">
|
|
46
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
47
|
+
<h2 className="text-3xl font-bold text-center mb-12">
|
|
48
|
+
Возможности PXLR CMS
|
|
49
|
+
</h2>
|
|
50
|
+
<div className="grid md:grid-cols-3 gap-8">
|
|
51
|
+
<div className="text-center p-6">
|
|
52
|
+
<div className="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
53
|
+
<span className="text-3xl">🚀</span>
|
|
54
|
+
</div>
|
|
55
|
+
<h3 className="text-xl font-semibold mb-2">Быстрый старт</h3>
|
|
56
|
+
<p className="text-gray-600">
|
|
57
|
+
Разверните CMS за минуты с помощью Docker. Всё готово к работе из коробки.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="text-center p-6">
|
|
61
|
+
<div className="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
62
|
+
<span className="text-3xl">🔒</span>
|
|
63
|
+
</div>
|
|
64
|
+
<h3 className="text-xl font-semibold mb-2">Полный контроль</h3>
|
|
65
|
+
<p className="text-gray-600">
|
|
66
|
+
Данные хранятся локально. Никаких облачных зависимостей и ежемесячных платежей.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="text-center p-6">
|
|
70
|
+
<div className="w-16 h-16 bg-purple-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
71
|
+
<span className="text-3xl">⚡</span>
|
|
72
|
+
</div>
|
|
73
|
+
<h3 className="text-xl font-semibold mb-2">REST API</h3>
|
|
74
|
+
<p className="text-gray-600">
|
|
75
|
+
Headless архитектура. Используйте контент где угодно через API.
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
{/* Latest Posts Section */}
|
|
83
|
+
{latestPosts.length > 0 && (
|
|
84
|
+
<section className="py-20 bg-gray-50">
|
|
85
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
86
|
+
<div className="flex justify-between items-center mb-12">
|
|
87
|
+
<h2 className="text-3xl font-bold">Последние записи</h2>
|
|
88
|
+
<Link
|
|
89
|
+
href="/blog"
|
|
90
|
+
className="text-blue-600 hover:text-blue-700 font-medium"
|
|
91
|
+
>
|
|
92
|
+
Все записи →
|
|
93
|
+
</Link>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="grid md:grid-cols-3 gap-8">
|
|
96
|
+
{latestPosts.map((post) => (
|
|
97
|
+
<article
|
|
98
|
+
key={post.id}
|
|
99
|
+
className="bg-white rounded-2xl border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow"
|
|
100
|
+
>
|
|
101
|
+
<div className="aspect-video bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center">
|
|
102
|
+
<span className="text-4xl">📄</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="p-6">
|
|
105
|
+
<div className="text-sm text-gray-500 mb-2">
|
|
106
|
+
{post.data.publishedAt &&
|
|
107
|
+
new Date(post.data.publishedAt).toLocaleDateString('ru-RU', {
|
|
108
|
+
year: 'numeric',
|
|
109
|
+
month: 'long',
|
|
110
|
+
day: 'numeric',
|
|
111
|
+
})}
|
|
112
|
+
</div>
|
|
113
|
+
<Link href={`/blog/${post.data.slug}`}>
|
|
114
|
+
<h3 className="text-xl font-semibold text-gray-900 mb-2 hover:text-blue-600 transition-colors">
|
|
115
|
+
{post.data.title}
|
|
116
|
+
</h3>
|
|
117
|
+
</Link>
|
|
118
|
+
{post.data.excerpt && (
|
|
119
|
+
<p className="text-gray-600 line-clamp-2">
|
|
120
|
+
{post.data.excerpt}
|
|
121
|
+
</p>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</article>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</section>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* CTA Section */}
|
|
132
|
+
<section className="py-20">
|
|
133
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
134
|
+
<div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-3xl p-12 text-center text-white">
|
|
135
|
+
<h2 className="text-3xl font-bold mb-4">
|
|
136
|
+
Начните управлять контентом
|
|
137
|
+
</h2>
|
|
138
|
+
<p className="text-blue-100 mb-8 max-w-2xl mx-auto">
|
|
139
|
+
Откройте админ-панель, создавайте схемы и добавляйте контент.
|
|
140
|
+
Всё просто и интуитивно понятно.
|
|
141
|
+
</p>
|
|
142
|
+
<a
|
|
143
|
+
href="http://localhost:3333"
|
|
144
|
+
target="_blank"
|
|
145
|
+
rel="noopener noreferrer"
|
|
146
|
+
className="inline-block bg-white text-blue-600 px-8 py-3 rounded-lg font-medium hover:bg-blue-50 transition-colors"
|
|
147
|
+
>
|
|
148
|
+
Открыть админ-панель
|
|
149
|
+
</a>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</section>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
images: {
|
|
5
|
+
remotePatterns: [
|
|
6
|
+
{
|
|
7
|
+
protocol: 'http',
|
|
8
|
+
hostname: 'localhost',
|
|
9
|
+
port: '9010',
|
|
10
|
+
pathname: '/**',
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default nextConfig;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pxlr-frontend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"next": "^15.1.0",
|
|
13
|
+
"react": "^18.3.1",
|
|
14
|
+
"react-dom": "^18.3.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.0.0",
|
|
18
|
+
"@types/react": "^18.3.0",
|
|
19
|
+
"@types/react-dom": "^18.3.0",
|
|
20
|
+
"typescript": "^5.6.0",
|
|
21
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
22
|
+
"tailwindcss": "^4.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|