@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +47 -0
  3. package/package.json +53 -0
  4. package/src/admin/admin.module.css +1312 -0
  5. package/src/admin/admin.types.ts +85 -0
  6. package/src/admin/components/access-denied.tsx +12 -0
  7. package/src/admin/components/admin-shell.tsx +168 -0
  8. package/src/admin/components/content-list-view.tsx +83 -0
  9. package/src/admin/components/dashboard-view.tsx +113 -0
  10. package/src/admin/components/data-table.tsx +80 -0
  11. package/src/admin/components/document-delete-button.tsx +44 -0
  12. package/src/admin/components/icons.tsx +150 -0
  13. package/src/admin/components/modal.tsx +78 -0
  14. package/src/admin/components/page-builder.tsx +1334 -0
  15. package/src/admin/components/settings-view.tsx +334 -0
  16. package/src/admin/components/sign-out-button.tsx +22 -0
  17. package/src/admin/components/system-view.tsx +77 -0
  18. package/src/admin/components/users-panel.tsx +321 -0
  19. package/src/admin/index.ts +20 -0
  20. package/src/admin/ocsm-admin.tsx +259 -0
  21. package/src/auth/authenticate.ts +76 -0
  22. package/src/auth/index.ts +9 -0
  23. package/src/auth/password.ts +22 -0
  24. package/src/auth/permissions.ts +27 -0
  25. package/src/auth/session.ts +103 -0
  26. package/src/blocks/block-renderer.tsx +428 -0
  27. package/src/blocks/block.types.ts +401 -0
  28. package/src/blocks/index.ts +15 -0
  29. package/src/blocks/markdown.tsx +11 -0
  30. package/src/config/config.schema.ts +28 -0
  31. package/src/config/config.types.ts +16 -0
  32. package/src/config/define-config.ts +19 -0
  33. package/src/config/index.ts +13 -0
  34. package/src/config/resolve-config.ts +10 -0
  35. package/src/content/content-repository.ts +66 -0
  36. package/src/content/content-store.interface.ts +23 -0
  37. package/src/content/content.types.ts +25 -0
  38. package/src/content/create-content-store.ts +18 -0
  39. package/src/content/frontmatter.ts +25 -0
  40. package/src/content/index.ts +12 -0
  41. package/src/index.ts +10 -0
  42. package/src/layout/index.ts +1 -0
  43. package/src/layout/layout-store.ts +27 -0
  44. package/src/roles/index.ts +10 -0
  45. package/src/roles/role-store.ts +95 -0
  46. package/src/roles/role.types.ts +86 -0
  47. package/src/server/create-ocsm.ts +67 -0
  48. package/src/server/documents.ts +28 -0
  49. package/src/server/index.ts +59 -0
  50. package/src/server/render-mdx.tsx +14 -0
  51. package/src/storage/create-file-backend.ts +26 -0
  52. package/src/storage/file-backend.ts +26 -0
  53. package/src/storage/fs-file-backend.ts +43 -0
  54. package/src/storage/github-file-backend.ts +97 -0
  55. package/src/storage/index.ts +8 -0
  56. package/src/storage/json-store.ts +23 -0
  57. package/src/theme/css.ts +28 -0
  58. package/src/theme/index.ts +8 -0
  59. package/src/theme/theme-store.ts +19 -0
  60. package/src/theme/theme.types.ts +53 -0
  61. package/src/types/css-modules.d.ts +4 -0
  62. package/src/update/check-for-updates.ts +50 -0
  63. package/src/update/index.ts +1 -0
  64. package/src/users/index.ts +6 -0
  65. package/src/users/user-store.ts +120 -0
  66. package/src/users/user.types.ts +18 -0
  67. 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
+ }