@orhancodestudio/ocsm-core 0.1.0-alpha.1
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/CHANGELOG.md +13 -0
- package/README.md +47 -0
- package/package.json +53 -0
- package/src/admin/admin.module.css +1312 -0
- package/src/admin/admin.types.ts +85 -0
- package/src/admin/components/access-denied.tsx +12 -0
- package/src/admin/components/admin-shell.tsx +168 -0
- package/src/admin/components/content-list-view.tsx +83 -0
- package/src/admin/components/dashboard-view.tsx +113 -0
- package/src/admin/components/data-table.tsx +80 -0
- package/src/admin/components/document-delete-button.tsx +44 -0
- package/src/admin/components/icons.tsx +150 -0
- package/src/admin/components/modal.tsx +78 -0
- package/src/admin/components/page-builder.tsx +1334 -0
- package/src/admin/components/settings-view.tsx +334 -0
- package/src/admin/components/sign-out-button.tsx +22 -0
- package/src/admin/components/system-view.tsx +77 -0
- package/src/admin/components/users-panel.tsx +321 -0
- package/src/admin/index.ts +20 -0
- package/src/admin/ocsm-admin.tsx +259 -0
- package/src/auth/authenticate.ts +76 -0
- package/src/auth/index.ts +9 -0
- package/src/auth/password.ts +22 -0
- package/src/auth/permissions.ts +27 -0
- package/src/auth/session.ts +103 -0
- package/src/blocks/block-renderer.tsx +428 -0
- package/src/blocks/block.types.ts +401 -0
- package/src/blocks/index.ts +15 -0
- package/src/blocks/markdown.tsx +11 -0
- package/src/config/config.schema.ts +28 -0
- package/src/config/config.types.ts +16 -0
- package/src/config/define-config.ts +19 -0
- package/src/config/index.ts +13 -0
- package/src/config/resolve-config.ts +10 -0
- package/src/content/content-repository.ts +66 -0
- package/src/content/content-store.interface.ts +23 -0
- package/src/content/content.types.ts +25 -0
- package/src/content/create-content-store.ts +18 -0
- package/src/content/frontmatter.ts +25 -0
- package/src/content/index.ts +12 -0
- package/src/index.ts +10 -0
- package/src/layout/index.ts +1 -0
- package/src/layout/layout-store.ts +27 -0
- package/src/roles/index.ts +10 -0
- package/src/roles/role-store.ts +95 -0
- package/src/roles/role.types.ts +86 -0
- package/src/server/create-ocsm.ts +67 -0
- package/src/server/documents.ts +28 -0
- package/src/server/index.ts +59 -0
- package/src/server/render-mdx.tsx +14 -0
- package/src/storage/create-file-backend.ts +26 -0
- package/src/storage/file-backend.ts +26 -0
- package/src/storage/fs-file-backend.ts +43 -0
- package/src/storage/github-file-backend.ts +97 -0
- package/src/storage/index.ts +8 -0
- package/src/storage/json-store.ts +23 -0
- package/src/theme/css.ts +28 -0
- package/src/theme/index.ts +8 -0
- package/src/theme/theme-store.ts +19 -0
- package/src/theme/theme.types.ts +53 -0
- package/src/types/css-modules.d.ts +4 -0
- package/src/update/check-for-updates.ts +50 -0
- package/src/update/index.ts +1 -0
- package/src/users/index.ts +6 -0
- package/src/users/user-store.ts +120 -0
- package/src/users/user.types.ts +18 -0
- package/src/version.ts +11 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Block } from "../blocks/block.types";
|
|
2
|
+
import type { LayoutRegion } from "../layout/layout-store";
|
|
3
|
+
import type { OcsmPermission } from "../roles/role.types";
|
|
4
|
+
import type { OcsmTheme } from "../theme/theme.types";
|
|
5
|
+
|
|
6
|
+
/** Payload sent from the page builder when saving a document. */
|
|
7
|
+
export interface SaveDocumentInput {
|
|
8
|
+
collection: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
title: string;
|
|
11
|
+
blocks: Block[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Result returned by admin server actions. */
|
|
15
|
+
export interface ActionResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
message?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SaveDocumentAction = (
|
|
21
|
+
input: SaveDocumentInput,
|
|
22
|
+
) => Promise<ActionResult>;
|
|
23
|
+
export type DeleteDocumentAction = (
|
|
24
|
+
collection: string,
|
|
25
|
+
slug: string,
|
|
26
|
+
) => Promise<ActionResult>;
|
|
27
|
+
|
|
28
|
+
// --- Users ---
|
|
29
|
+
export interface CreateUserActionInput {
|
|
30
|
+
username: string;
|
|
31
|
+
name: string;
|
|
32
|
+
role: string;
|
|
33
|
+
password: string;
|
|
34
|
+
}
|
|
35
|
+
export interface UpdateUserActionInput {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
role: string;
|
|
39
|
+
password?: string;
|
|
40
|
+
}
|
|
41
|
+
export type CreateUserAction = (
|
|
42
|
+
input: CreateUserActionInput,
|
|
43
|
+
) => Promise<ActionResult>;
|
|
44
|
+
export type UpdateUserAction = (
|
|
45
|
+
input: UpdateUserActionInput,
|
|
46
|
+
) => Promise<ActionResult>;
|
|
47
|
+
export type DeleteUserAction = (id: string) => Promise<ActionResult>;
|
|
48
|
+
|
|
49
|
+
// --- Roles ---
|
|
50
|
+
export interface RoleActionInput {
|
|
51
|
+
name: string;
|
|
52
|
+
permissions: OcsmPermission[];
|
|
53
|
+
}
|
|
54
|
+
export type CreateRoleAction = (
|
|
55
|
+
input: RoleActionInput,
|
|
56
|
+
) => Promise<ActionResult>;
|
|
57
|
+
export type UpdateRoleAction = (
|
|
58
|
+
id: string,
|
|
59
|
+
input: RoleActionInput,
|
|
60
|
+
) => Promise<ActionResult>;
|
|
61
|
+
export type DeleteRoleAction = (id: string) => Promise<ActionResult>;
|
|
62
|
+
|
|
63
|
+
export type SaveThemeAction = (theme: OcsmTheme) => Promise<ActionResult>;
|
|
64
|
+
|
|
65
|
+
export type SaveLayoutAction = (
|
|
66
|
+
region: LayoutRegion,
|
|
67
|
+
blocks: Block[],
|
|
68
|
+
) => Promise<ActionResult>;
|
|
69
|
+
|
|
70
|
+
export type SignOutAction = () => Promise<void>;
|
|
71
|
+
|
|
72
|
+
/** The full set of server actions the admin panel needs from the host app. */
|
|
73
|
+
export interface OcsmAdminActions {
|
|
74
|
+
saveDocument: SaveDocumentAction;
|
|
75
|
+
deleteDocument: DeleteDocumentAction;
|
|
76
|
+
createUser: CreateUserAction;
|
|
77
|
+
updateUser: UpdateUserAction;
|
|
78
|
+
deleteUser: DeleteUserAction;
|
|
79
|
+
createRole: CreateRoleAction;
|
|
80
|
+
updateRole: UpdateRoleAction;
|
|
81
|
+
deleteRole: DeleteRoleAction;
|
|
82
|
+
saveTheme: SaveThemeAction;
|
|
83
|
+
saveLayout: SaveLayoutAction;
|
|
84
|
+
signOut: SignOutAction;
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import styles from "../admin.module.css";
|
|
2
|
+
|
|
3
|
+
export function AccessDenied() {
|
|
4
|
+
return (
|
|
5
|
+
<div className={styles.accessDenied}>
|
|
6
|
+
<h1 className={styles.pageTitle}>Erişim engellendi</h1>
|
|
7
|
+
<p className={styles.pageSubtitle}>
|
|
8
|
+
Bu bölümü görüntülemek için yeterli yetkiniz yok.
|
|
9
|
+
</p>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { SessionUser } from "../../auth/session";
|
|
3
|
+
import type { ResolvedOcsmConfig } from "../../config/config.types";
|
|
4
|
+
import { OCSM_VERSION } from "../../version";
|
|
5
|
+
import type { SignOutAction } from "../admin.types";
|
|
6
|
+
import styles from "../admin.module.css";
|
|
7
|
+
import {
|
|
8
|
+
IconDashboard,
|
|
9
|
+
IconDocument,
|
|
10
|
+
IconInfo,
|
|
11
|
+
IconSettings,
|
|
12
|
+
IconUsers,
|
|
13
|
+
} from "./icons";
|
|
14
|
+
import { SignOutButton } from "./sign-out-button";
|
|
15
|
+
|
|
16
|
+
export interface AdminShellProps {
|
|
17
|
+
brand: ResolvedOcsmConfig["brand"];
|
|
18
|
+
collections: ResolvedOcsmConfig["collections"];
|
|
19
|
+
currentUser: SessionUser;
|
|
20
|
+
active: string;
|
|
21
|
+
title: string;
|
|
22
|
+
canManageUsers: boolean;
|
|
23
|
+
canManageSettings: boolean;
|
|
24
|
+
signOut: SignOutAction;
|
|
25
|
+
updateLatest: string | null;
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Chrome for the admin panel: branded sidebar, top bar, navigation. */
|
|
30
|
+
export function AdminShell({
|
|
31
|
+
brand,
|
|
32
|
+
collections,
|
|
33
|
+
currentUser,
|
|
34
|
+
active,
|
|
35
|
+
title,
|
|
36
|
+
canManageUsers,
|
|
37
|
+
canManageSettings,
|
|
38
|
+
signOut,
|
|
39
|
+
updateLatest,
|
|
40
|
+
children,
|
|
41
|
+
}: AdminShellProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div className={styles.app}>
|
|
44
|
+
<aside className={styles.sidebar}>
|
|
45
|
+
<div className={styles.brand}>
|
|
46
|
+
<span className={styles.brandName}>{brand.name}</span>
|
|
47
|
+
<span className={styles.brandMeta}>OCS Management</span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<nav className={styles.nav}>
|
|
51
|
+
<NavLink
|
|
52
|
+
href="/ocsm-admin"
|
|
53
|
+
label="Panel"
|
|
54
|
+
icon={<IconDashboard className={styles.navIcon} />}
|
|
55
|
+
active={active === "dashboard"}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<div className={styles.navSectionTitle}>İçerik</div>
|
|
59
|
+
{collections.map((collection) => (
|
|
60
|
+
<NavLink
|
|
61
|
+
key={collection.name}
|
|
62
|
+
href={`/ocsm-admin/${collection.name}`}
|
|
63
|
+
label={collection.label}
|
|
64
|
+
icon={<IconDocument className={styles.navIcon} />}
|
|
65
|
+
active={active === collection.name}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
|
|
69
|
+
<div className={styles.navSectionTitle}>Yönetim</div>
|
|
70
|
+
{canManageUsers ? (
|
|
71
|
+
<NavLink
|
|
72
|
+
href="/ocsm-admin/users"
|
|
73
|
+
label="Kullanıcılar"
|
|
74
|
+
icon={<IconUsers className={styles.navIcon} />}
|
|
75
|
+
active={active === "users"}
|
|
76
|
+
/>
|
|
77
|
+
) : null}
|
|
78
|
+
{canManageSettings ? (
|
|
79
|
+
<NavLink
|
|
80
|
+
href="/ocsm-admin/settings"
|
|
81
|
+
label="Yapılandırma"
|
|
82
|
+
icon={<IconSettings className={styles.navIcon} />}
|
|
83
|
+
active={active === "settings"}
|
|
84
|
+
/>
|
|
85
|
+
) : null}
|
|
86
|
+
<NavLink
|
|
87
|
+
href="/ocsm-admin/system"
|
|
88
|
+
label="Sistem"
|
|
89
|
+
icon={<IconInfo className={styles.navIcon} />}
|
|
90
|
+
active={active === "system"}
|
|
91
|
+
/>
|
|
92
|
+
</nav>
|
|
93
|
+
|
|
94
|
+
<div className={styles.sidebarFooter}>
|
|
95
|
+
{updateLatest ? (
|
|
96
|
+
<a
|
|
97
|
+
className={styles.updateBadge}
|
|
98
|
+
href="/ocsm-admin/system"
|
|
99
|
+
>
|
|
100
|
+
Güncelleme mevcut: v{updateLatest}
|
|
101
|
+
</a>
|
|
102
|
+
) : (
|
|
103
|
+
<div className={styles.versionTag}>OCSM v{OCSM_VERSION}</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</aside>
|
|
107
|
+
|
|
108
|
+
<div className={styles.main}>
|
|
109
|
+
<header className={styles.topbar}>
|
|
110
|
+
<div className={styles.topbarTitle}>{title}</div>
|
|
111
|
+
<div className={styles.topbarRight}>
|
|
112
|
+
<div className={styles.userChip}>
|
|
113
|
+
<span className={styles.userAvatar}>
|
|
114
|
+
{initials(currentUser.name)}
|
|
115
|
+
</span>
|
|
116
|
+
<span className={styles.userMeta}>
|
|
117
|
+
<span className={styles.userName}>{currentUser.name}</span>
|
|
118
|
+
<span
|
|
119
|
+
className={`${styles.roleBadge} ${
|
|
120
|
+
currentUser.role === "admin"
|
|
121
|
+
? styles.roleAdmin
|
|
122
|
+
: styles.roleEditor
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{currentUser.roleName}
|
|
126
|
+
</span>
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
<SignOutButton signOut={signOut} />
|
|
130
|
+
</div>
|
|
131
|
+
</header>
|
|
132
|
+
|
|
133
|
+
<main className={styles.content}>{children}</main>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function NavLink({
|
|
140
|
+
href,
|
|
141
|
+
label,
|
|
142
|
+
icon,
|
|
143
|
+
active,
|
|
144
|
+
}: {
|
|
145
|
+
href: string;
|
|
146
|
+
label: string;
|
|
147
|
+
icon: ReactNode;
|
|
148
|
+
active: boolean;
|
|
149
|
+
}) {
|
|
150
|
+
return (
|
|
151
|
+
<a
|
|
152
|
+
href={href}
|
|
153
|
+
className={`${styles.navItem} ${active ? styles.navItemActive : ""}`}
|
|
154
|
+
>
|
|
155
|
+
{icon}
|
|
156
|
+
{label}
|
|
157
|
+
</a>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function initials(name: string): string {
|
|
162
|
+
return name
|
|
163
|
+
.split(/\s+/)
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
.slice(0, 2)
|
|
166
|
+
.map((part) => part[0]?.toUpperCase() ?? "")
|
|
167
|
+
.join("");
|
|
168
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { DeleteDocumentAction } from "../admin.types";
|
|
2
|
+
import styles from "../admin.module.css";
|
|
3
|
+
import { DataTable, type DataTableColumn } from "./data-table";
|
|
4
|
+
import { DocumentDeleteButton } from "./document-delete-button";
|
|
5
|
+
import { IconPlus } from "./icons";
|
|
6
|
+
|
|
7
|
+
export interface ContentListItem {
|
|
8
|
+
slug: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ContentListViewProps {
|
|
13
|
+
collection: string;
|
|
14
|
+
collectionLabel: string;
|
|
15
|
+
documents: ContentListItem[];
|
|
16
|
+
canDelete: boolean;
|
|
17
|
+
deleteAction: DeleteDocumentAction;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ContentListView({
|
|
21
|
+
collection,
|
|
22
|
+
collectionLabel,
|
|
23
|
+
documents,
|
|
24
|
+
canDelete,
|
|
25
|
+
deleteAction,
|
|
26
|
+
}: ContentListViewProps) {
|
|
27
|
+
const columns: DataTableColumn<ContentListItem>[] = [
|
|
28
|
+
{ key: "title", header: "Başlık", cell: (doc) => doc.title },
|
|
29
|
+
{
|
|
30
|
+
key: "slug",
|
|
31
|
+
header: "Slug",
|
|
32
|
+
cell: (doc) => <span className={styles.badge}>{doc.slug}</span>,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "actions",
|
|
36
|
+
header: "",
|
|
37
|
+
align: "right",
|
|
38
|
+
cell: (doc) => (
|
|
39
|
+
<div className={styles.tableActions}>
|
|
40
|
+
<a
|
|
41
|
+
className={`${styles.btn} ${styles.btnSm}`}
|
|
42
|
+
href={`/ocsm-admin/${collection}/${doc.slug}`}
|
|
43
|
+
>
|
|
44
|
+
Düzenle
|
|
45
|
+
</a>
|
|
46
|
+
{canDelete ? (
|
|
47
|
+
<DocumentDeleteButton
|
|
48
|
+
collection={collection}
|
|
49
|
+
slug={doc.slug}
|
|
50
|
+
title={doc.title}
|
|
51
|
+
deleteAction={deleteAction}
|
|
52
|
+
/>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
<div className={styles.pageHeader}>
|
|
62
|
+
<div>
|
|
63
|
+
<h1 className={styles.pageTitle}>{collectionLabel}</h1>
|
|
64
|
+
<p className={styles.pageSubtitle}>{documents.length} içerik</p>
|
|
65
|
+
</div>
|
|
66
|
+
<a
|
|
67
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
68
|
+
href={`/ocsm-admin/${collection}/new`}
|
|
69
|
+
>
|
|
70
|
+
<IconPlus width={16} height={16} />
|
|
71
|
+
Yeni
|
|
72
|
+
</a>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<DataTable
|
|
76
|
+
columns={columns}
|
|
77
|
+
rows={documents}
|
|
78
|
+
rowKey={(doc) => doc.slug}
|
|
79
|
+
emptyText="Henüz içerik yok. “Yeni” ile ilk içeriğini oluştur."
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import styles from "../admin.module.css";
|
|
2
|
+
import { DataTable, type DataTableColumn } from "./data-table";
|
|
3
|
+
import { IconDocument } from "./icons";
|
|
4
|
+
|
|
5
|
+
export interface DashboardStat {
|
|
6
|
+
name: string;
|
|
7
|
+
label: string;
|
|
8
|
+
count: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DashboardRecentItem {
|
|
12
|
+
collection: string;
|
|
13
|
+
collectionLabel: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
title: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DashboardViewProps {
|
|
19
|
+
currentUserName: string;
|
|
20
|
+
stats: DashboardStat[];
|
|
21
|
+
recent: DashboardRecentItem[];
|
|
22
|
+
version: string;
|
|
23
|
+
updateLatest: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DashboardView({
|
|
27
|
+
currentUserName,
|
|
28
|
+
stats,
|
|
29
|
+
recent,
|
|
30
|
+
version,
|
|
31
|
+
updateLatest,
|
|
32
|
+
}: DashboardViewProps) {
|
|
33
|
+
const recentColumns: DataTableColumn<DashboardRecentItem>[] = [
|
|
34
|
+
{ key: "title", header: "Başlık", cell: (item) => item.title },
|
|
35
|
+
{
|
|
36
|
+
key: "collection",
|
|
37
|
+
header: "Koleksiyon",
|
|
38
|
+
cell: (item) => (
|
|
39
|
+
<span className={styles.badge}>{item.collectionLabel}</span>
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "actions",
|
|
44
|
+
header: "",
|
|
45
|
+
align: "right",
|
|
46
|
+
cell: (item) => (
|
|
47
|
+
<div className={styles.tableActions}>
|
|
48
|
+
<a
|
|
49
|
+
className={`${styles.btn} ${styles.btnSm}`}
|
|
50
|
+
href={`/ocsm-admin/${item.collection}/${item.slug}`}
|
|
51
|
+
>
|
|
52
|
+
Düzenle
|
|
53
|
+
</a>
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
<div className={styles.pageHeader}>
|
|
62
|
+
<div>
|
|
63
|
+
<h1 className={styles.pageTitle}>Hoş geldin, {currentUserName}</h1>
|
|
64
|
+
<p className={styles.pageSubtitle}>
|
|
65
|
+
İçeriklerini buradan yönetebilirsin.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className={styles.cardGrid}>
|
|
71
|
+
{stats.map((stat) => (
|
|
72
|
+
<a
|
|
73
|
+
key={stat.name}
|
|
74
|
+
href={`/ocsm-admin/${stat.name}`}
|
|
75
|
+
className={styles.card}
|
|
76
|
+
>
|
|
77
|
+
<span className={styles.cardLabel}>
|
|
78
|
+
<IconDocument width={15} height={15} />
|
|
79
|
+
{stat.label}
|
|
80
|
+
</span>
|
|
81
|
+
<span className={styles.cardValue}>{stat.count}</span>
|
|
82
|
+
<span className={styles.cardHint}>içerik</span>
|
|
83
|
+
</a>
|
|
84
|
+
))}
|
|
85
|
+
<div className={styles.card}>
|
|
86
|
+
<span className={styles.cardLabel}>Sürüm</span>
|
|
87
|
+
<span className={styles.cardValue} style={{ fontSize: 18 }}>
|
|
88
|
+
v{version}
|
|
89
|
+
</span>
|
|
90
|
+
<span className={styles.cardHint}>
|
|
91
|
+
{updateLatest ? (
|
|
92
|
+
<span className={styles.badgeWarn} style={{ padding: "2px 8px" }}>
|
|
93
|
+
Güncelleme: v{updateLatest}
|
|
94
|
+
</span>
|
|
95
|
+
) : (
|
|
96
|
+
<span className={styles.badgeOk} style={{ padding: "2px 8px" }}>
|
|
97
|
+
Güncel
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<DataTable
|
|
105
|
+
title="Son içerikler"
|
|
106
|
+
columns={recentColumns}
|
|
107
|
+
rows={recent}
|
|
108
|
+
rowKey={(item) => `${item.collection}/${item.slug}`}
|
|
109
|
+
emptyText="Henüz içerik eklenmemiş."
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import styles from "../admin.module.css";
|
|
3
|
+
|
|
4
|
+
export interface DataTableColumn<T> {
|
|
5
|
+
/** Stable key for the column. */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Header label (empty string for an actions column). */
|
|
8
|
+
header: ReactNode;
|
|
9
|
+
/** Renders the cell content for a row. */
|
|
10
|
+
cell: (row: T) => ReactNode;
|
|
11
|
+
align?: "left" | "right";
|
|
12
|
+
width?: number | string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DataTableProps<T> {
|
|
16
|
+
columns: DataTableColumn<T>[];
|
|
17
|
+
rows: T[];
|
|
18
|
+
/** Stable React key for each row. */
|
|
19
|
+
rowKey: (row: T) => string;
|
|
20
|
+
/** Shown when there are no rows. */
|
|
21
|
+
emptyText?: string;
|
|
22
|
+
/** Optional panel header title above the table. */
|
|
23
|
+
title?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reusable table inside a panel — shared chrome (header, empty state, thead/tbody)
|
|
28
|
+
* for every admin list (content, users, dashboard…). Cell content is provided by
|
|
29
|
+
* the caller via each column's `cell` renderer.
|
|
30
|
+
*/
|
|
31
|
+
export function DataTable<T>({
|
|
32
|
+
columns,
|
|
33
|
+
rows,
|
|
34
|
+
rowKey,
|
|
35
|
+
emptyText = "Kayıt bulunamadı.",
|
|
36
|
+
title,
|
|
37
|
+
}: DataTableProps<T>) {
|
|
38
|
+
return (
|
|
39
|
+
<div className={styles.panel}>
|
|
40
|
+
{title ? (
|
|
41
|
+
<div className={styles.panelHeader}>
|
|
42
|
+
<h2 className={styles.panelTitle}>{title}</h2>
|
|
43
|
+
</div>
|
|
44
|
+
) : null}
|
|
45
|
+
|
|
46
|
+
{rows.length === 0 ? (
|
|
47
|
+
<div className={styles.emptyState}>{emptyText}</div>
|
|
48
|
+
) : (
|
|
49
|
+
<table className={styles.table}>
|
|
50
|
+
<thead>
|
|
51
|
+
<tr>
|
|
52
|
+
{columns.map((column) => (
|
|
53
|
+
<th
|
|
54
|
+
key={column.key}
|
|
55
|
+
style={{ textAlign: column.align ?? "left", width: column.width }}
|
|
56
|
+
>
|
|
57
|
+
{column.header}
|
|
58
|
+
</th>
|
|
59
|
+
))}
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
{rows.map((row) => (
|
|
64
|
+
<tr key={rowKey(row)}>
|
|
65
|
+
{columns.map((column) => (
|
|
66
|
+
<td
|
|
67
|
+
key={column.key}
|
|
68
|
+
style={{ textAlign: column.align ?? "left" }}
|
|
69
|
+
>
|
|
70
|
+
{column.cell(row)}
|
|
71
|
+
</td>
|
|
72
|
+
))}
|
|
73
|
+
</tr>
|
|
74
|
+
))}
|
|
75
|
+
</tbody>
|
|
76
|
+
</table>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useTransition } from "react";
|
|
5
|
+
import type { DeleteDocumentAction } from "../admin.types";
|
|
6
|
+
import styles from "../admin.module.css";
|
|
7
|
+
import { IconTrash } from "./icons";
|
|
8
|
+
|
|
9
|
+
export interface DocumentDeleteButtonProps {
|
|
10
|
+
collection: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
title: string;
|
|
13
|
+
deleteAction: DeleteDocumentAction;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DocumentDeleteButton({
|
|
17
|
+
collection,
|
|
18
|
+
slug,
|
|
19
|
+
title,
|
|
20
|
+
deleteAction,
|
|
21
|
+
}: DocumentDeleteButtonProps) {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const [pending, startTransition] = useTransition();
|
|
24
|
+
|
|
25
|
+
function onClick() {
|
|
26
|
+
if (!window.confirm(`"${title}" silinsin mi?`)) return;
|
|
27
|
+
startTransition(async () => {
|
|
28
|
+
await deleteAction(collection, slug);
|
|
29
|
+
router.refresh();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
className={`${styles.btn} ${styles.btnSm} ${styles.btnDanger}`}
|
|
37
|
+
disabled={pending}
|
|
38
|
+
onClick={onClick}
|
|
39
|
+
>
|
|
40
|
+
<IconTrash width={14} height={14} />
|
|
41
|
+
{pending ? "…" : "Sil"}
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
}
|