fse-theme-default 0.3.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.
Files changed (47) hide show
  1. package/README.md +55 -0
  2. package/astro.config.mjs +22 -0
  3. package/package.json +38 -0
  4. package/public/favicon.svg +4 -0
  5. package/src/assets/logo.svg +5 -0
  6. package/src/components/Card.astro +31 -0
  7. package/src/components/Head.astro +9 -0
  8. package/src/components/Header.astro +55 -0
  9. package/src/components/Input.astro +56 -0
  10. package/src/components/Logo.astro +17 -0
  11. package/src/components/Modal.astro +78 -0
  12. package/src/components/NavLink.astro +23 -0
  13. package/src/components/Pagination.astro +81 -0
  14. package/src/components/SearchBar.astro +65 -0
  15. package/src/components/Select.astro +60 -0
  16. package/src/components/Sidebar.astro +207 -0
  17. package/src/components/SidebarLinks.astro +10 -0
  18. package/src/components/Table.astro +157 -0
  19. package/src/components/TableFilters.astro +137 -0
  20. package/src/env.d.ts +2 -0
  21. package/src/layouts/EmailLayout.astro +45 -0
  22. package/src/layouts/Layout.astro +95 -0
  23. package/src/pages/emails/password-reset.astro +39 -0
  24. package/src/pages/emails/verify-email-change.astro +39 -0
  25. package/src/pages/emails/verify.astro +39 -0
  26. package/src/pages/error.astro +26 -0
  27. package/src/pages/forgot-password.astro +67 -0
  28. package/src/pages/fse/form.astro +169 -0
  29. package/src/pages/fse/links.astro +104 -0
  30. package/src/pages/fse/list.astro +166 -0
  31. package/src/pages/fse/public-detail.astro +29 -0
  32. package/src/pages/fse/public-list.astro +54 -0
  33. package/src/pages/index.astro +41 -0
  34. package/src/pages/login.astro +88 -0
  35. package/src/pages/public/error.astro +29 -0
  36. package/src/pages/register-success.astro +31 -0
  37. package/src/pages/register.astro +99 -0
  38. package/src/pages/reset-password.astro +117 -0
  39. package/src/pages/settings.astro +188 -0
  40. package/src/pages/user.astro +65 -0
  41. package/src/pages/users.astro +52 -0
  42. package/src/styles/global.css +11 -0
  43. package/src/styles/theme.css +408 -0
  44. package/src/types/pages.ts +58 -0
  45. package/src/types.ts +154 -0
  46. package/theme.json +5 -0
  47. package/tsconfig.json +5 -0
@@ -0,0 +1,188 @@
1
+ ---
2
+ import { ssr } from "fse-ssr/ssr";
3
+ import Layout from "../layouts/Layout.astro";
4
+ import Card from "../components/Card.astro";
5
+ import Header from "../components/Header.astro";
6
+ import Input from "../components/Input.astro";
7
+ import Modal from "../components/Modal.astro";
8
+ import type { SettingsPage } from "../types/pages";
9
+
10
+ const {
11
+ t,
12
+ current_email,
13
+ first_name,
14
+ last_name,
15
+ success,
16
+ error,
17
+ email_success,
18
+ email_error,
19
+ } = ssr<SettingsPage>();
20
+
21
+ const messages = t.messages as Record<string, string>;
22
+ const errors = t.errors as Record<string, string>;
23
+ ---
24
+
25
+ <Layout title={t.settings.title}>
26
+ <main class="max-w-2xl mx-auto">
27
+ <Header>{t.settings.title}</Header>
28
+
29
+ <Card>
30
+ <div class="flex flex-col divide-y divide-secondary-100">
31
+
32
+ <!-- Name -->
33
+ {(first_name || last_name) && (
34
+ <div class="pb-6">
35
+ <h3 class="text-base font-semibold text-secondary-900">
36
+ {t.settings.name}
37
+ </h3>
38
+ <div class="mt-3 px-4 py-3 rounded-lg bg-secondary-50 border border-secondary-100">
39
+ <span class="text-sm font-medium text-secondary-900">
40
+ {first_name} {last_name}
41
+ </span>
42
+ </div>
43
+ </div>
44
+ )}
45
+
46
+ <!-- Email Setting -->
47
+ <div class="py-6 first:pt-0">
48
+ <div class="flex items-start justify-between mb-3 flex-wrap gap-2">
49
+ <div>
50
+ <h3 class="text-base font-semibold text-secondary-900">
51
+ {t.settings.email_address}
52
+ </h3>
53
+ <p class="text-sm text-secondary-500 mt-0.5">
54
+ {t.settings.email_desc}
55
+ </p>
56
+ </div>
57
+ <button type="button" id="change-email-btn" class="btn-secondary shrink-0">
58
+ {t.settings.change_email}
59
+ </button>
60
+ </div>
61
+ <div class="px-4 py-3 rounded-lg bg-secondary-50 border border-secondary-100">
62
+ <span class="text-sm font-medium text-secondary-900">
63
+ {current_email}
64
+ </span>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Password Setting -->
69
+ <div class="py-6">
70
+ <div class="mb-4">
71
+ <h3 class="text-base font-semibold text-secondary-900">
72
+ {t.settings.account_security}
73
+ </h3>
74
+ </div>
75
+ <form action="/settings/password-reset" method="POST" data-astro-reload>
76
+ <button type="submit" class="btn-secondary">
77
+ {t.settings.send_reset}
78
+ </button>
79
+ </form>
80
+ {success && (
81
+ <div class="mt-4 px-4 py-3 rounded-lg bg-primary-50 border border-primary-200">
82
+ <p class="text-sm font-medium text-primary-700">{messages[success] ?? success}</p>
83
+ </div>
84
+ )}
85
+ {error && (
86
+ <div class="mt-4 px-4 py-3 rounded-lg bg-red-50 border border-red-200">
87
+ <p class="text-sm font-medium text-red-700">{errors[error] ?? error}</p>
88
+ </div>
89
+ )}
90
+ </div>
91
+
92
+ <!-- Danger Zone -->
93
+ <div class="pt-6">
94
+ <div class="flex items-start justify-between flex-wrap gap-3">
95
+ <div>
96
+ <h3 class="text-base font-semibold text-red-600">
97
+ {t.settings.delete_account}
98
+ </h3>
99
+ <p class="text-sm text-secondary-500 mt-0.5">
100
+ {t.settings.delete_account_desc}
101
+ </p>
102
+ </div>
103
+ <button type="button" id="delete-account-btn" class="btn-danger shrink-0">
104
+ {t.settings.delete_account}
105
+ </button>
106
+ </div>
107
+ </div>
108
+
109
+ </div>
110
+ </Card>
111
+ </main>
112
+
113
+ <!-- Change Email Modal -->
114
+ <Modal id="change-email-modal" title={t.settings.update_email_title}>
115
+ <form id="change-email-form" action="/settings/change-email" method="POST" data-astro-reload class="flex flex-col gap-5">
116
+ <div class="flex flex-col gap-2">
117
+ <label for="new_email" class="text-sm font-medium text-secondary-700">
118
+ {t.settings.new_email}
119
+ </label>
120
+ <Input id="new_email" type="email" placeholder="e.g. name@example.com" required={true} />
121
+ </div>
122
+ <div class="flex flex-col gap-2">
123
+ <button type="submit" class="w-full btn-primary">
124
+ {t.settings.update_email_btn}
125
+ </button>
126
+ <button type="button" id="cancel-email-change-btn" class="w-full btn-secondary">
127
+ {t.settings.cancel}
128
+ </button>
129
+ </div>
130
+ <div id="modal-messages">
131
+ {email_success && (
132
+ <div class="px-4 py-3 rounded-lg bg-primary-50 border border-primary-200">
133
+ <p class="text-sm font-medium text-primary-700">{messages[email_success] ?? email_success}</p>
134
+ </div>
135
+ )}
136
+ {email_error && (
137
+ <div class="px-4 py-3 rounded-lg bg-red-50 border border-red-200">
138
+ <p class="text-sm font-medium text-red-700">{errors[email_error] ?? email_error}</p>
139
+ </div>
140
+ )}
141
+ </div>
142
+ </form>
143
+ </Modal>
144
+
145
+ <!-- Delete Account Modal -->
146
+ <Modal id="delete-account-modal" title={t.settings.delete_account_title}>
147
+ <p class="text-sm text-secondary-600 mb-5">
148
+ {t.settings.delete_account_confirm}
149
+ </p>
150
+ <div class="flex flex-col gap-2">
151
+ <form action="/settings/delete-account" method="POST" data-astro-reload>
152
+ <button type="submit" class="w-full px-4 py-2 inline-flex items-center justify-center text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition-colors">
153
+ {t.settings.delete_account_btn}
154
+ </button>
155
+ </form>
156
+ <button type="button" id="cancel-delete-account-btn" class="w-full btn-secondary">
157
+ {t.settings.cancel}
158
+ </button>
159
+ </div>
160
+ </Modal>
161
+
162
+ <script>
163
+ function setupSettings() {
164
+ const modal = document.getElementById("change-email-modal") as HTMLDialogElement;
165
+ const openBtn = document.getElementById("change-email-btn");
166
+ const closeBtn = document.getElementById("cancel-email-change-btn");
167
+ openBtn?.addEventListener("click", () => {
168
+ if (modal && !modal.open) modal.showModal();
169
+ });
170
+ closeBtn?.addEventListener("click", () => modal?.close());
171
+
172
+ const messages = document.getElementById("modal-messages");
173
+ if (messages && (messages.textContent?.trim().length ?? 0) > 0 && modal && !modal.open) {
174
+ modal.showModal();
175
+ }
176
+
177
+ const deleteAccountModal = document.getElementById("delete-account-modal") as HTMLDialogElement;
178
+ document.getElementById("delete-account-btn")?.addEventListener("click", () => {
179
+ if (deleteAccountModal && !deleteAccountModal.open) deleteAccountModal.showModal();
180
+ });
181
+ document.getElementById("cancel-delete-account-btn")?.addEventListener("click", () => deleteAccountModal?.close());
182
+ }
183
+
184
+ // `astro:page-load` replaces DOMContentLoaded, which does not fire on
185
+ // client-side navigation. It also fires on the initial load.
186
+ document.addEventListener("astro:page-load", setupSettings);
187
+ </script>
188
+ </Layout>
@@ -0,0 +1,65 @@
1
+ ---
2
+ import { ssr } from "fse-ssr/ssr";
3
+ import Card from "../components/Card.astro";
4
+ import Header from "../components/Header.astro";
5
+ import Layout from "../layouts/Layout.astro";
6
+ import type { UserPage } from "../types/pages";
7
+
8
+ const { t, id, email, role, roles } = ssr<UserPage>();
9
+ const roleLabels = t.roles as Record<string, string>;
10
+ ---
11
+
12
+ <Layout title={t.users.edit}>
13
+ <main class="max-w-2xl mx-auto w-full">
14
+ <Header backlink="/users">{t.users.edit_title}</Header>
15
+ <Card>
16
+ <form action={`/users/${id}`} method="POST" data-astro-reload class="flex flex-col gap-6">
17
+ <!-- Email (read-only) -->
18
+ <div>
19
+ <label class="block text-sm font-medium text-secondary-700 mb-2">
20
+ {t.users.email}
21
+ </label>
22
+ <div class="px-4 py-2.5 rounded-lg bg-secondary-50 border border-secondary-100">
23
+ <span class="text-sm font-medium text-secondary-900">
24
+ {email}
25
+ </span>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Role -->
30
+ <div class="relative w-full">
31
+ <select
32
+ id="role"
33
+ name="role"
34
+ required
35
+ class="relative w-full h-10 px-3.5 text-sm transition-all bg-white border border-secondary-300 rounded-lg outline-hidden appearance-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 peer text-secondary-900"
36
+ >
37
+ {
38
+ roles.map((r) => (
39
+ <option value={r.value} selected={r.value === role}>
40
+ {roleLabels[r.value] ?? r.label}
41
+ </option>
42
+ ))
43
+ }
44
+ </select>
45
+ <label
46
+ for="role"
47
+ class="pointer-events-none absolute top-2.5 left-2 z-1 px-1.5 text-sm text-secondary-500 transition-all before:absolute before:top-0 before:left-0 before:z-[-1] before:block before:h-full before:w-full before:bg-white before:transition-all peer-valid:-top-2 peer-valid:text-xs peer-focus:-top-2 peer-focus:text-xs peer-focus:text-primary-600"
48
+ >
49
+ {t.users.role}
50
+ </label>
51
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-secondary-400">
52
+ <path d="m6 9 6 6 6-6"></path>
53
+ </svg>
54
+ </div>
55
+
56
+ <!-- Actions -->
57
+ <div class="flex sm:justify-end pt-5 border-t border-secondary-100">
58
+ <button type="submit" class="btn-primary w-full sm:w-auto">
59
+ <span>{t.common.save}</span>
60
+ </button>
61
+ </div>
62
+ </form>
63
+ </Card>
64
+ </main>
65
+ </Layout>
@@ -0,0 +1,52 @@
1
+ ---
2
+ import { ssr } from "fse-ssr/ssr";
3
+ import Card from "../components/Card.astro";
4
+ import Layout from "../layouts/Layout.astro";
5
+ import Table from "../components/Table.astro";
6
+ import Header from "../components/Header.astro";
7
+ import TableFilters from "../components/TableFilters.astro";
8
+ import Pagination from "../components/Pagination.astro";
9
+ import type { TableHeader, UsersPage } from "../types/pages";
10
+
11
+ const { t, rows, filter_role, roles } = ssr<UsersPage>();
12
+
13
+ const headers: TableHeader[] = [
14
+ { title: "ID", key: "id" },
15
+ { title: "Email", key: "email" },
16
+ { title: "Role", key: "role" },
17
+ { title: "Date", key: "created_at" },
18
+ { title: "Actions", key: "id", format: "delete" },
19
+ ];
20
+
21
+ const roleLabels = t.roles as Record<string, string>;
22
+ ---
23
+
24
+ <Layout title={t.users.title}>
25
+ <main>
26
+ <Header>{t.users.title}</Header>
27
+ <Card padding={false}>
28
+ <TableFilters searchPlaceholder={t.filters.search_users ?? "Search by email..."}>
29
+ <div class="relative">
30
+ <select
31
+ name="filter_role"
32
+ class="h-9 pl-3 pr-8 text-sm border border-secondary-200 rounded-lg bg-white focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-hidden text-secondary-700 appearance-none"
33
+ >
34
+ <option value="">{t.filters.all_roles ?? "All roles"}</option>
35
+ {
36
+ roles.map((role) => (
37
+ <option value={role.value} selected={filter_role === role.value}>
38
+ {roleLabels[role.value] ?? role.label}
39
+ </option>
40
+ ))
41
+ }
42
+ </select>
43
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-secondary-400">
44
+ <path d="m6 9 6 6 6-6"></path>
45
+ </svg>
46
+ </div>
47
+ </TableFilters>
48
+ <Table headers={headers} rows={rows} />
49
+ <Pagination />
50
+ </Card>
51
+ </main>
52
+ </Layout>
@@ -0,0 +1,11 @@
1
+ /*
2
+ The theme's Tailwind root, imported by the layout. A child theme replaces
3
+ this whole file by creating its own src/styles/global.css — typically:
4
+
5
+ @import "tailwindcss";
6
+ @import "fse-theme-default/src/styles/theme.css";
7
+ @source "../";
8
+ @theme { --color-primary-600: #1d4ed8; } (overrides)
9
+ */
10
+ @import "tailwindcss";
11
+ @import "./theme.css";