@kyro-cms/admin 0.12.4 → 0.12.5
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/dist/index.cjs +64 -26
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +64 -26
- package/package.json +9 -3
- package/src/components/ActionBar.tsx +16 -16
- package/src/components/ApiHealth.tsx +522 -0
- package/src/components/ApiKeysManager.tsx +6 -4
- package/src/components/AuditLogsPage.tsx +4 -2
- package/src/components/AutoForm.tsx +10 -7
- package/src/components/BrandingHub.tsx +3 -1
- package/src/components/Dashboard.tsx +44 -37
- package/src/components/DashboardMetrics.tsx +168 -58
- package/src/components/DetailView.tsx +41 -38
- package/src/components/DeveloperCenter.tsx +5 -3
- package/src/components/GraphQLPlayground.tsx +10 -8
- package/src/components/ListView.tsx +66 -45
- package/src/components/LoginPage.tsx +21 -19
- package/src/components/MarketplaceManager.tsx +3 -1
- package/src/components/MediaGallery.tsx +12 -9
- package/src/components/PluginsManager.tsx +6 -4
- package/src/components/RestPlayground.tsx +8 -6
- package/src/components/SessionsManager.tsx +3 -1
- package/src/components/Sidebar.astro +180 -92
- package/src/components/UserManagement.tsx +8 -6
- package/src/components/UserMenu.tsx +19 -17
- package/src/components/VersionHistoryPanel.tsx +6 -4
- package/src/components/WebhookManager.tsx +11 -10
- package/src/components/autoform/AutoFormApiView.tsx +3 -1
- package/src/components/autoform/AutoFormEditView.tsx +3 -1
- package/src/components/autoform/AutoFormHeader.tsx +6 -6
- package/src/components/blocks/AccordionBlock.tsx +5 -3
- package/src/components/blocks/ArrayBlock.tsx +5 -3
- package/src/components/blocks/BlockWrapper.tsx +5 -3
- package/src/components/blocks/CodeBlock.tsx +4 -2
- package/src/components/blocks/FileBlock.tsx +5 -3
- package/src/components/blocks/HeroBlock.tsx +5 -3
- package/src/components/blocks/ImageBlock.tsx +6 -4
- package/src/components/blocks/ListBlock.tsx +5 -3
- package/src/components/blocks/ParagraphBlock.tsx +3 -1
- package/src/components/blocks/RelationshipBlock.tsx +5 -3
- package/src/components/blocks/RichTextBlock.tsx +5 -3
- package/src/components/blocks/VideoBlock.tsx +5 -3
- package/src/components/fields/AccordionField.tsx +10 -51
- package/src/components/fields/ArrayField.tsx +9 -7
- package/src/components/fields/ArrayLayout.tsx +3 -1
- package/src/components/fields/BlocksField.tsx +24 -75
- package/src/components/fields/ButtonField.tsx +3 -1
- package/src/components/fields/CardField.tsx +6 -4
- package/src/components/fields/GroupLayout.tsx +2 -49
- package/src/components/fields/HeadingField.tsx +3 -1
- package/src/components/fields/HeadingSubheadingField.tsx +4 -2
- package/src/components/fields/HeroField.tsx +8 -6
- package/src/components/fields/JSONField.tsx +9 -10
- package/src/components/fields/LinkField.tsx +4 -2
- package/src/components/fields/ListField.tsx +3 -1
- package/src/components/fields/MarkdownField.tsx +3 -17
- package/src/components/fields/RelationshipBlockField.tsx +7 -5
- package/src/components/fields/RelationshipField.tsx +3 -1
- package/src/components/fields/RichTextField.tsx +19 -16
- package/src/components/fields/SecretField.tsx +9 -69
- package/src/components/fields/SelectField.tsx +11 -6
- package/src/components/fields/TabsLayout.tsx +2 -48
- package/src/components/fields/TextField.tsx +9 -7
- package/src/components/fields/UploadField.tsx +3 -1
- package/src/components/fields/VideoField.tsx +6 -4
- package/src/components/ui/BlockDrawer.tsx +3 -1
- package/src/components/ui/CommandPalette.tsx +9 -7
- package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
- package/src/components/ui/IconPickerModal.tsx +4 -2
- package/src/components/ui/PageHeader.tsx +48 -19
- package/src/components/users/UserDetail.tsx +3 -1
- package/src/components/users/UserForm.tsx +5 -3
- package/src/fields/examples/sample-field-2.tsx +3 -1
- package/src/fields/examples/sample-field.tsx +3 -1
- package/src/hooks/useAutoFormState.ts +19 -11
- package/src/integration.ts +21 -0
- package/src/layouts/AdminLayout.astro +252 -189
- package/src/layouts/AuthLayout.astro +52 -31
- package/src/lib/api.ts +7 -7
- package/src/lib/deep-equal.ts +3 -3
- package/src/lib/i18n.ts +44 -0
- package/src/lib/vite-shim-plugin.ts +3 -0
- package/src/locales/de.json +163 -0
- package/src/locales/en.json +163 -0
- package/src/locales/es.json +163 -0
- package/src/locales/fr.json +163 -0
- package/src/locales/pt.json +163 -0
- package/src/pages/403.astro +2 -1
- package/src/pages/[collection]/[id].astro +23 -10
- package/src/pages/[collection]/index.astro +48 -34
- package/src/pages/audit/index.astro +2 -1
- package/src/pages/graphql-explorer.astro +2 -1
- package/src/pages/graphql.astro +2 -1
- package/src/pages/health.astro +9 -0
- package/src/pages/index.astro +6 -5
- package/src/pages/keys.astro +2 -1
- package/src/pages/marketplace.astro +2 -1
- package/src/pages/media.astro +2 -1
- package/src/pages/plugins.astro +2 -1
- package/src/pages/rest-playground.astro +2 -1
- package/src/pages/roles/index.astro +16 -52
- package/src/pages/sessions.astro +2 -1
- package/src/pages/settings/[slug].astro +45 -30
- package/src/pages/users/[id].astro +1 -0
- package/src/pages/users/index.astro +2 -1
- package/src/pages/users/new.astro +2 -1
- package/src/pages/webhooks.astro +2 -1
- package/src/pages/index_ALT.astro +0 -3
|
@@ -197,6 +197,7 @@ import { javascript } from "@codemirror/lang-javascript";
|
|
|
197
197
|
import { json } from "@codemirror/lang-json";
|
|
198
198
|
import { CompletionContext, autocompletion } from "@codemirror/autocomplete";
|
|
199
199
|
import { aura } from "@uiw/codemirror-theme-aura";
|
|
200
|
+
import { useTranslation } from "react-i18next";
|
|
200
201
|
|
|
201
202
|
function prettifyQuery(query: string): string {
|
|
202
203
|
let indent = 0;
|
|
@@ -421,6 +422,7 @@ export function GraphQLPlayground({
|
|
|
421
422
|
initialVariables,
|
|
422
423
|
initialShowDocs = false,
|
|
423
424
|
}: GraphQLPlaygroundProps) {
|
|
425
|
+
const { t } = useTranslation();
|
|
424
426
|
const [token, setToken] = useState<string>("");
|
|
425
427
|
const [showToken, setShowToken] = useState(false);
|
|
426
428
|
const [isConnected, setIsConnected] = useState(false);
|
|
@@ -837,7 +839,7 @@ export function GraphQLPlayground({
|
|
|
837
839
|
type={showToken ? "text" : "password"}
|
|
838
840
|
value={token}
|
|
839
841
|
onChange={(e) => setToken(e.target.value)}
|
|
840
|
-
placeholder="API key"
|
|
842
|
+
placeholder={t("fields.apiKey", { defaultValue: "API key" })}
|
|
841
843
|
className="w-24 px-2 py-1 text-[10px] font-mono bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
842
844
|
/>
|
|
843
845
|
<button onClick={() => setShowToken(!showToken)} className="p-1 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]">
|
|
@@ -846,22 +848,22 @@ export function GraphQLPlayground({
|
|
|
846
848
|
</div>
|
|
847
849
|
)}
|
|
848
850
|
<div className="ml-auto flex items-center gap-1 flex-wrap justify-end">
|
|
849
|
-
<button onClick={() => { setShowDocs(!showDocs); setRightTab("docs"); }} className={`p-1.5 rounded-lg transition-all ${rightTab === "docs" && showDocs ? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title="Schema docs">
|
|
851
|
+
<button onClick={() => { setShowDocs(!showDocs); setRightTab("docs"); }} className={`p-1.5 rounded-lg transition-all ${rightTab === "docs" && showDocs ? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title={t("tooltips.schemaDocs", { defaultValue: "Schema docs" })}>
|
|
850
852
|
<Book className="w-3.5 h-3.5" />
|
|
851
853
|
</button>
|
|
852
|
-
<button onClick={() => setRightTab("history")} className={`p-1.5 rounded-lg transition-all ${rightTab === "history" ? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title="History">
|
|
854
|
+
<button onClick={() => setRightTab("history")} className={`p-1.5 rounded-lg transition-all ${rightTab === "history" ? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title={t("tooltips.history", { defaultValue: "History" })}>
|
|
853
855
|
<Clock className="w-3.5 h-3.5" />
|
|
854
856
|
</button>
|
|
855
|
-
<button onClick={handlePrettify} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Prettify (Cmd+Shift+P)">
|
|
857
|
+
<button onClick={handlePrettify} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.prettifyCmdshiftp", { defaultValue: "Prettify (Cmd+Shift+P)" })}>
|
|
856
858
|
<Code2 className="w-3.5 h-3.5" />
|
|
857
859
|
</button>
|
|
858
|
-
<button onClick={handleCopyResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Copy response">
|
|
860
|
+
<button onClick={handleCopyResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.copyResponse", { defaultValue: "Copy response" })}>
|
|
859
861
|
{copied ? <Check className="w-3.5 h-3.5 text-[var(--kyro-success)]" /> : <Copy className="w-3.5 h-3.5" />}
|
|
860
862
|
</button>
|
|
861
|
-
<button onClick={handleDownloadResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Download response">
|
|
863
|
+
<button onClick={handleDownloadResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.downloadResponse", { defaultValue: "Download response" })}>
|
|
862
864
|
<Download className="w-3.5 h-3.5" />
|
|
863
865
|
</button>
|
|
864
|
-
<button onClick={handleClearEditor} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger-bg)]" title="Clear editor">
|
|
866
|
+
<button onClick={handleClearEditor} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger-bg)]" title={t("tooltips.clearEditor", { defaultValue: "Clear editor" })}>
|
|
865
867
|
<Trash2 className="w-3.5 h-3.5" />
|
|
866
868
|
</button>
|
|
867
869
|
<div className="h-4 w-px bg-[var(--kyro-border)] mx-1" />
|
|
@@ -977,7 +979,7 @@ export function GraphQLPlayground({
|
|
|
977
979
|
type="text"
|
|
978
980
|
value={searchQuery}
|
|
979
981
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
980
|
-
placeholder="Search types and fields..."
|
|
982
|
+
placeholder={t("fields.searchTypesAndFields", { defaultValue: "Search types and fields..." })}
|
|
981
983
|
className="w-full pl-7 pr-6 py-1 text-[10px] bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
982
984
|
/>
|
|
983
985
|
{searchQuery && (
|
|
@@ -11,7 +11,8 @@ import { adminPath as ADMIN_BASE } from "../lib/paths";
|
|
|
11
11
|
import { PageHeader } from "./ui/PageHeader";
|
|
12
12
|
import { Badge } from "./ui/Badge";
|
|
13
13
|
import { Pagination } from "./ui/Pagination";
|
|
14
|
-
|
|
14
|
+
import { useTranslation } from "react-i18next";
|
|
15
|
+
import "../lib/i18n";
|
|
15
16
|
|
|
16
17
|
import type { CollectionConfig, Field } from "@kyro-cms/core";
|
|
17
18
|
import { resolveFieldValue } from "../lib/resolve-field-value";
|
|
@@ -60,6 +61,7 @@ export function ListView({
|
|
|
60
61
|
onEdit: providedOnEdit,
|
|
61
62
|
config,
|
|
62
63
|
}: ListViewProps) {
|
|
64
|
+
const { t } = useTranslation();
|
|
63
65
|
const collectionSlug = providedSlug || collection.slug;
|
|
64
66
|
const { permissions } = useAuthStore();
|
|
65
67
|
const canCreate = permissions?.collections?.[collectionSlug]?.create !== false;
|
|
@@ -71,7 +73,13 @@ export function ListView({
|
|
|
71
73
|
if (providedOnCreate) {
|
|
72
74
|
providedOnCreate();
|
|
73
75
|
} else {
|
|
74
|
-
|
|
76
|
+
const href = `${ADMIN_BASE}/${collectionSlug}/new`;
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
import("astro:transitions/client").then(({ navigate }) => {
|
|
79
|
+
navigate(href);
|
|
80
|
+
}).catch(() => {
|
|
81
|
+
window.location.href = href;
|
|
82
|
+
});
|
|
75
83
|
}
|
|
76
84
|
};
|
|
77
85
|
|
|
@@ -79,7 +87,13 @@ export function ListView({
|
|
|
79
87
|
if (providedOnEdit) {
|
|
80
88
|
providedOnEdit(id);
|
|
81
89
|
} else {
|
|
82
|
-
|
|
90
|
+
const href = `${ADMIN_BASE}/${collectionSlug}/${id}`;
|
|
91
|
+
// @ts-ignore
|
|
92
|
+
import("astro:transitions/client").then(({ navigate }) => {
|
|
93
|
+
navigate(href);
|
|
94
|
+
}).catch(() => {
|
|
95
|
+
window.location.href = href;
|
|
96
|
+
});
|
|
83
97
|
}
|
|
84
98
|
};
|
|
85
99
|
|
|
@@ -201,17 +215,24 @@ export function ListView({
|
|
|
201
215
|
|
|
202
216
|
const displayFields = useMemo(
|
|
203
217
|
() => {
|
|
204
|
-
const fields
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
const fields: (FieldConfig & { name: string })[] = [];
|
|
219
|
+
for (const colName of Array.from(visibleColumns)) {
|
|
220
|
+
if (colName === "status") {
|
|
221
|
+
fields.push({
|
|
222
|
+
name: "status",
|
|
223
|
+
type: "select",
|
|
224
|
+
label: "Status",
|
|
225
|
+
options: [
|
|
226
|
+
{ value: "draft", label: "Draft" },
|
|
227
|
+
{ value: "published", label: "Published" },
|
|
228
|
+
],
|
|
229
|
+
} as any);
|
|
230
|
+
} else {
|
|
231
|
+
const field = allFields.find((f) => f.name === colName);
|
|
232
|
+
if (field && field.name) {
|
|
233
|
+
fields.push(field as typeof field & { name: string });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
215
236
|
}
|
|
216
237
|
return fields;
|
|
217
238
|
},
|
|
@@ -297,8 +318,8 @@ export function ListView({
|
|
|
297
318
|
|
|
298
319
|
const handleBulkDelete = () => {
|
|
299
320
|
confirm({
|
|
300
|
-
title: "Delete Documents",
|
|
301
|
-
message:
|
|
321
|
+
title: t("listView.deleteBulkTitle", { defaultValue: "Delete Documents" }),
|
|
322
|
+
message: t("listView.deleteBulkMessage", { defaultValue: "Are you sure you want to delete {{count}} document(s)? This cannot be undone.", count: selectedIds.size }),
|
|
302
323
|
variant: "danger",
|
|
303
324
|
onConfirm: async () => {
|
|
304
325
|
try {
|
|
@@ -318,8 +339,8 @@ export function ListView({
|
|
|
318
339
|
|
|
319
340
|
const handleDeleteSingle = (id: string) => {
|
|
320
341
|
confirm({
|
|
321
|
-
title: "Delete Document",
|
|
322
|
-
message: "Are you sure you want to delete this document? This cannot be undone.",
|
|
342
|
+
title: t("listView.deleteTitle", { defaultValue: "Delete Document" }),
|
|
343
|
+
message: t("listView.deleteMessage", { defaultValue: "Are you sure you want to delete this document? This cannot be undone." }),
|
|
323
344
|
variant: "danger",
|
|
324
345
|
onConfirm: async () => {
|
|
325
346
|
try {
|
|
@@ -348,8 +369,9 @@ export function ListView({
|
|
|
348
369
|
</span>
|
|
349
370
|
] : undefined}
|
|
350
371
|
action={canCreate ? {
|
|
351
|
-
label:
|
|
352
|
-
|
|
372
|
+
label: t("actions.create", { defaultValue: "Create {{item}}", item: collection.singularLabel || collection.label || collectionSlug }),
|
|
373
|
+
href: providedOnCreate ? undefined : `${ADMIN_BASE}/${collectionSlug}/new`,
|
|
374
|
+
onClick: providedOnCreate ? handleCreate : undefined,
|
|
353
375
|
icon: Plus,
|
|
354
376
|
} : undefined}
|
|
355
377
|
|
|
@@ -363,7 +385,7 @@ export function ListView({
|
|
|
363
385
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
364
386
|
<input
|
|
365
387
|
type="text"
|
|
366
|
-
placeholder="Search..."
|
|
388
|
+
placeholder={t("listView.searchPlaceholder", { defaultValue: "Search..." })}
|
|
367
389
|
value={search}
|
|
368
390
|
onChange={(e) => setSearch(e.target.value)}
|
|
369
391
|
className="w-full pl-10 pr-4 py-2.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-sm font-medium text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
@@ -381,7 +403,7 @@ export function ListView({
|
|
|
381
403
|
}`}
|
|
382
404
|
>
|
|
383
405
|
<Filter className="w-4 h-4" />
|
|
384
|
-
Filters
|
|
406
|
+
{t("listView.filters", { defaultValue: "Filters" })}
|
|
385
407
|
{filters.length > 0 && (
|
|
386
408
|
<span className="ml-1 px-1.5 py-0.5 bg-[var(--kyro-sidebar-text-active)] text-[var(--kyro-sidebar-active)] rounded-full text-xs">
|
|
387
409
|
{filters.length}
|
|
@@ -397,13 +419,13 @@ export function ListView({
|
|
|
397
419
|
className="flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
|
|
398
420
|
>
|
|
399
421
|
<Columns3 className="w-4 h-4" />
|
|
400
|
-
Columns
|
|
422
|
+
{t("listView.columns", { defaultValue: "Columns" })}
|
|
401
423
|
</button>
|
|
402
424
|
{showColumns && (
|
|
403
425
|
<div className="absolute right-0 top-full mt-2 w-56 surface-tile border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 overflow-hidden">
|
|
404
426
|
<div className="p-3 border-b border-[var(--kyro-border)]">
|
|
405
427
|
<span className="text-xs font-bold tracking-wider text-[var(--kyro-text-secondary)]">
|
|
406
|
-
Toggle Columns
|
|
428
|
+
{t("listView.toggleColumns", { defaultValue: "Toggle Columns" })}
|
|
407
429
|
</span>
|
|
408
430
|
</div>
|
|
409
431
|
<div className="p-2 max-h-64 overflow-y-auto">
|
|
@@ -438,7 +460,7 @@ export function ListView({
|
|
|
438
460
|
onClick={clearAll}
|
|
439
461
|
className="px-4 py-2 rounded-xl font-bold text-sm text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-all"
|
|
440
462
|
>
|
|
441
|
-
Clear All
|
|
463
|
+
{t("listView.clearAll", { defaultValue: "Clear All" })}
|
|
442
464
|
</button>
|
|
443
465
|
)}
|
|
444
466
|
</div>
|
|
@@ -449,7 +471,7 @@ export function ListView({
|
|
|
449
471
|
<div className="surface-tile p-4 border-l-4 border-[var(--kyro-sidebar-active)]">
|
|
450
472
|
<div className="flex items-center justify-between mb-4">
|
|
451
473
|
<h3 className="font-medium text-[var(--kyro-text-primary)]">
|
|
452
|
-
Advanced Filters
|
|
474
|
+
{t("listView.advancedFilters", { defaultValue: "Advanced Filters" })}
|
|
453
475
|
</h3>
|
|
454
476
|
<button
|
|
455
477
|
type="button"
|
|
@@ -457,7 +479,7 @@ export function ListView({
|
|
|
457
479
|
className="flex items-center gap-2 px-3 py-1.5 text-sm font-bold text-[var(--kyro-sidebar-active)] hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all"
|
|
458
480
|
>
|
|
459
481
|
<Plus className="w-4 h-4" />
|
|
460
|
-
Add Filter
|
|
482
|
+
{t("listView.addFilter", { defaultValue: "Add Filter" })}
|
|
461
483
|
</button>
|
|
462
484
|
</div>
|
|
463
485
|
<div className="space-y-3">
|
|
@@ -498,7 +520,7 @@ export function ListView({
|
|
|
498
520
|
onChange={(e) =>
|
|
499
521
|
updateFilter(index, { value: e.target.value })
|
|
500
522
|
}
|
|
501
|
-
placeholder="Value..."
|
|
523
|
+
placeholder={t("fields.value", { defaultValue: "Value..." })}
|
|
502
524
|
className="flex-1 min-w-[150px] px-3 py-2 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm font-medium text-[var(--kyro-text-primary)]"
|
|
503
525
|
/>
|
|
504
526
|
<button
|
|
@@ -512,7 +534,7 @@ export function ListView({
|
|
|
512
534
|
))}
|
|
513
535
|
{filters.length === 0 && (
|
|
514
536
|
<p className="text-sm text-[var(--kyro-text-muted)]">
|
|
515
|
-
No filters applied. Click "Add Filter" to create one.
|
|
537
|
+
{t("listView.noFilters", { defaultValue: 'No filters applied. Click "Add Filter" to create one.' })}
|
|
516
538
|
</p>
|
|
517
539
|
)}
|
|
518
540
|
</div>
|
|
@@ -523,7 +545,7 @@ export function ListView({
|
|
|
523
545
|
{selectedIds.size > 0 && (
|
|
524
546
|
<div className="surface-tile p-4 flex items-center justify-between border-l-4 border-[var(--kyro-sidebar-active)]">
|
|
525
547
|
<span className="text-sm font-medium text-[var(--kyro-text-primary)]">
|
|
526
|
-
{selectedIds.size}
|
|
548
|
+
{t("listView.selectedCount", { defaultValue: "{{count}} selected", count: selectedIds.size })}
|
|
527
549
|
</span>
|
|
528
550
|
<div className="flex gap-2">
|
|
529
551
|
{canDelete && (
|
|
@@ -533,7 +555,7 @@ export function ListView({
|
|
|
533
555
|
className="flex items-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg font-bold text-sm hover:bg-red-600 transition-all"
|
|
534
556
|
>
|
|
535
557
|
<Trash2 className="w-4 h-4" />
|
|
536
|
-
Delete Selected
|
|
558
|
+
{t("listView.deleteSelected", { defaultValue: "Delete Selected" })}
|
|
537
559
|
</button>
|
|
538
560
|
)}
|
|
539
561
|
<button
|
|
@@ -541,7 +563,7 @@ export function ListView({
|
|
|
541
563
|
onClick={() => setSelectedIds(new Set())}
|
|
542
564
|
className="px-4 py-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] font-bold text-sm transition-all"
|
|
543
565
|
>
|
|
544
|
-
Cancel
|
|
566
|
+
{t("actions.cancel", { defaultValue: "Cancel" })}
|
|
545
567
|
</button>
|
|
546
568
|
</div>
|
|
547
569
|
</div>
|
|
@@ -559,12 +581,12 @@ export function ListView({
|
|
|
559
581
|
<Archive className="w-4 h-4" />
|
|
560
582
|
</div>
|
|
561
583
|
<p className="font-medium text-[var(--kyro-text-primary)] text-base">
|
|
562
|
-
No documents found
|
|
584
|
+
{t("listView.noDocuments", { defaultValue: "No documents found" })}
|
|
563
585
|
</p>
|
|
564
586
|
<p className="text-sm text-[var(--kyro-text-secondary)] mt-1">
|
|
565
587
|
{hasActiveFilters
|
|
566
|
-
? "Try adjusting your filters or search query."
|
|
567
|
-
:
|
|
588
|
+
? t("listView.tryAdjustingFilters", { defaultValue: "Try adjusting your filters or search query." })
|
|
589
|
+
: t("listView.getStartedCreate", { defaultValue: "Get started by creating your first {{item}}.", item: ((collection.singularLabel || collection.label || collectionSlug) as string).toLowerCase() })}
|
|
568
590
|
</p>
|
|
569
591
|
{!hasActiveFilters && canCreate && (
|
|
570
592
|
<button
|
|
@@ -573,8 +595,7 @@ export function ListView({
|
|
|
573
595
|
className="mt-4 kyro-btn kyro-btn-md kyro-btn-primary shadow-md flex items-center gap-2"
|
|
574
596
|
>
|
|
575
597
|
<Plus className="w-4 h-4" />
|
|
576
|
-
|
|
577
|
-
{String(collection.singularLabel || collection.label || collectionSlug)}
|
|
598
|
+
{t("actions.create", { defaultValue: "Create {{item}}", item: String(collection.singularLabel || collection.label || collectionSlug) })}
|
|
578
599
|
</button>
|
|
579
600
|
)}
|
|
580
601
|
</div>
|
|
@@ -609,12 +630,12 @@ export function ListView({
|
|
|
609
630
|
</th>
|
|
610
631
|
))}
|
|
611
632
|
{collection.timestamps ? (
|
|
612
|
-
<th className="px-4 py-4">Created</th>
|
|
633
|
+
<th className="px-4 py-4">{t("listView.createdCol", { defaultValue: "Created" })}</th>
|
|
613
634
|
) : null}
|
|
614
635
|
{collection.timestamps ? (
|
|
615
|
-
<th className="px-4 py-4">Last Modified</th>
|
|
636
|
+
<th className="px-4 py-4">{t("listView.lastModifiedCol", { defaultValue: "Last Modified" })}</th>
|
|
616
637
|
) : null}
|
|
617
|
-
<th className="px-4 py-4 text-right">Actions</th>
|
|
638
|
+
<th className="px-4 py-4 text-right">{t("listView.actionsCol", { defaultValue: "Actions" })}</th>
|
|
618
639
|
</tr>
|
|
619
640
|
</thead>
|
|
620
641
|
<tbody className="divide-y divide-[var(--kyro-border)]">
|
|
@@ -641,7 +662,7 @@ export function ListView({
|
|
|
641
662
|
field.type === "select" && rawValue && Array.isArray(field.options)
|
|
642
663
|
? field.options.find((o: any) => o.value === rawValue)
|
|
643
664
|
?.label || rawValue
|
|
644
|
-
: formatCellValue(rawValue, field.type);
|
|
665
|
+
: formatCellValue(rawValue, field.type, t);
|
|
645
666
|
return (
|
|
646
667
|
<td
|
|
647
668
|
key={field.name}
|
|
@@ -688,7 +709,7 @@ export function ListView({
|
|
|
688
709
|
type="button"
|
|
689
710
|
onClick={() => handleEdit(doc.id)}
|
|
690
711
|
className="flex items-center gap-2 px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
|
|
691
|
-
title={canUpdate ? "Edit" : "View"}
|
|
712
|
+
title={canUpdate ? t("actions.edit", { defaultValue: "Edit" }) : t("actions.view", { defaultValue: "View" })}
|
|
692
713
|
>
|
|
693
714
|
<Edit2 className="w-4 h-4" />
|
|
694
715
|
</button>
|
|
@@ -697,7 +718,7 @@ export function ListView({
|
|
|
697
718
|
type="button"
|
|
698
719
|
onClick={() => handleDeleteSingle(doc.id)}
|
|
699
720
|
className="inline-flex items-center justify-center w-8 h-8 rounded-md text-[var(--kyro-text-muted)] hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10 transition-colors"
|
|
700
|
-
title="Delete"
|
|
721
|
+
title={t("actions.delete", { defaultValue: "Delete" })}
|
|
701
722
|
>
|
|
702
723
|
<Trash2 className="w-4 h-4" />
|
|
703
724
|
</button>
|
|
@@ -728,9 +749,9 @@ export function ListView({
|
|
|
728
749
|
);
|
|
729
750
|
}
|
|
730
751
|
|
|
731
|
-
function formatCellValue(value: any, type?: string): string {
|
|
752
|
+
function formatCellValue(value: any, type?: string, t?: any): string {
|
|
732
753
|
if (value === null || value === undefined) return "—";
|
|
733
|
-
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
754
|
+
if (typeof value === "boolean") return value ? (t ? t("common.yes", { defaultValue: "Yes" }) : "Yes") : (t ? t("common.no", { defaultValue: "No" }) : "No");
|
|
734
755
|
if (type === "number" || type === "price") return String(value);
|
|
735
756
|
if (type === "date" || type === "datetime") {
|
|
736
757
|
return new Date(value).toLocaleDateString("en-US", {
|
|
@@ -3,6 +3,7 @@ import { apiGet, apiPost } from "../lib/api";
|
|
|
3
3
|
import { ThemeProvider, type ThemeMode } from "./ThemeProvider";
|
|
4
4
|
import { Toaster } from "./ui/Toaster";
|
|
5
5
|
import { useToastStore } from "../lib/stores";
|
|
6
|
+
import { useTranslation } from "react-i18next";
|
|
6
7
|
|
|
7
8
|
interface LoginPageProps {
|
|
8
9
|
onAuth: (token: string, user: Record<string, unknown>) => void;
|
|
@@ -12,6 +13,7 @@ interface LoginPageProps {
|
|
|
12
13
|
type AuthMode = "login" | "register";
|
|
13
14
|
|
|
14
15
|
export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
16
|
+
const { t } = useTranslation();
|
|
15
17
|
const [mode, setMode] = useState<AuthMode>("login");
|
|
16
18
|
const [email, setEmail] = useState("");
|
|
17
19
|
const [password, setPassword] = useState("");
|
|
@@ -54,11 +56,11 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
|
54
56
|
localStorage.setItem("kyro_user", JSON.stringify(data.user));
|
|
55
57
|
addToast(
|
|
56
58
|
"success",
|
|
57
|
-
mode === "login" ? "Welcome back!" : "Account created!",
|
|
59
|
+
mode === "login" ? t("toast.welcomeBack", { defaultValue: "Welcome back!" }) : t("toast.accountCreated", { defaultValue: "Account created!" }),
|
|
58
60
|
);
|
|
59
61
|
onAuth(data.token, data.user);
|
|
60
62
|
} catch {
|
|
61
|
-
addToast("error", "Connection failed");
|
|
63
|
+
addToast("error", t("toast.connectionFailed", { defaultValue: "Connection failed" }));
|
|
62
64
|
} finally {
|
|
63
65
|
setLoading(false);
|
|
64
66
|
}
|
|
@@ -71,23 +73,23 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
|
71
73
|
<div className="kyro-login-header">
|
|
72
74
|
<h1 className="kyro-login-title">
|
|
73
75
|
{isFirstUser
|
|
74
|
-
? "Create Admin Account"
|
|
76
|
+
? t("login.createAdminAccount", { defaultValue: "Create Admin Account" })
|
|
75
77
|
: mode === "login"
|
|
76
|
-
? "Sign In"
|
|
77
|
-
: "Create Account"}
|
|
78
|
+
? t("login.signIn", { defaultValue: "Sign In" })
|
|
79
|
+
: t("login.createAccount", { defaultValue: "Create Account" })}
|
|
78
80
|
</h1>
|
|
79
81
|
<p className="kyro-login-subtitle">
|
|
80
82
|
{isFirstUser
|
|
81
|
-
? "Set up your admin account to get started"
|
|
83
|
+
? t("login.setupAdmin", { defaultValue: "Set up your admin account to get started" })
|
|
82
84
|
: mode === "login"
|
|
83
|
-
? "Enter your credentials to access the admin"
|
|
84
|
-
: "Create an account to access the admin"}
|
|
85
|
+
? t("login.enterCredentials", { defaultValue: "Enter your credentials to access the admin" })
|
|
86
|
+
: t("login.createAccountDesc", { defaultValue: "Create an account to access the admin" })}
|
|
85
87
|
</p>
|
|
86
88
|
</div>
|
|
87
89
|
|
|
88
90
|
<form onSubmit={handleSubmit} className="kyro-login-form">
|
|
89
91
|
<div className="kyro-form-group">
|
|
90
|
-
<label htmlFor="email">Email</label>
|
|
92
|
+
<label htmlFor="email">{t("login.email", { defaultValue: "Email" })}</label>
|
|
91
93
|
<input
|
|
92
94
|
id="email"
|
|
93
95
|
type="email"
|
|
@@ -100,7 +102,7 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
|
100
102
|
</div>
|
|
101
103
|
|
|
102
104
|
<div className="kyro-form-group">
|
|
103
|
-
<label htmlFor="password">Password</label>
|
|
105
|
+
<label htmlFor="password">{t("login.password", { defaultValue: "Password" })}</label>
|
|
104
106
|
<input
|
|
105
107
|
id="password"
|
|
106
108
|
type="password"
|
|
@@ -117,7 +119,7 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
|
117
119
|
|
|
118
120
|
{mode === "register" && (
|
|
119
121
|
<div className="kyro-form-group">
|
|
120
|
-
<label htmlFor="confirmPassword">Confirm Password</label>
|
|
122
|
+
<label htmlFor="confirmPassword">{t("login.confirmPassword", { defaultValue: "Confirm Password" })}</label>
|
|
121
123
|
<input
|
|
122
124
|
id="confirmPassword"
|
|
123
125
|
type="password"
|
|
@@ -138,11 +140,11 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
|
138
140
|
>
|
|
139
141
|
{loading
|
|
140
142
|
? mode === "login"
|
|
141
|
-
? "Signing in..."
|
|
142
|
-
: "Creating account..."
|
|
143
|
+
? t("login.signingIn", { defaultValue: "Signing in..." })
|
|
144
|
+
: t("login.creatingAccount", { defaultValue: "Creating account..." })
|
|
143
145
|
: mode === "login"
|
|
144
|
-
? "Sign In"
|
|
145
|
-
: "Create Account"}
|
|
146
|
+
? t("login.signIn", { defaultValue: "Sign In" })
|
|
147
|
+
: t("login.createAccount", { defaultValue: "Create Account" })}
|
|
146
148
|
</button>
|
|
147
149
|
</form>
|
|
148
150
|
|
|
@@ -151,24 +153,24 @@ export function LoginPage({ onAuth, theme = "light" }: LoginPageProps) {
|
|
|
151
153
|
<p>
|
|
152
154
|
{mode === "login" ? (
|
|
153
155
|
<>
|
|
154
|
-
Don't have an account?{" "}
|
|
156
|
+
{t("login.noAccount", { defaultValue: "Don't have an account?" })}{" "}
|
|
155
157
|
<button
|
|
156
158
|
type="button"
|
|
157
159
|
className="kyro-login-link"
|
|
158
160
|
onClick={() => setMode("register")}
|
|
159
161
|
>
|
|
160
|
-
Sign up
|
|
162
|
+
{t("login.signUp", { defaultValue: "Sign up" })}
|
|
161
163
|
</button>
|
|
162
164
|
</>
|
|
163
165
|
) : (
|
|
164
166
|
<>
|
|
165
|
-
Already have an account?{" "}
|
|
167
|
+
{t("login.hasAccount", { defaultValue: "Already have an account?" })}{" "}
|
|
166
168
|
<button
|
|
167
169
|
type="button"
|
|
168
170
|
className="kyro-login-link"
|
|
169
171
|
onClick={() => setMode("login")}
|
|
170
172
|
>
|
|
171
|
-
Sign in
|
|
173
|
+
{t("login.signInAction", { defaultValue: "Sign in" })}
|
|
172
174
|
</button>
|
|
173
175
|
</>
|
|
174
176
|
)}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "./ui/icons";
|
|
13
13
|
import { useUIStore, toast } from "../lib/stores";
|
|
14
14
|
import { Badge } from "./ui/Badge";
|
|
15
|
+
import { useTranslation } from "react-i18next";
|
|
15
16
|
|
|
16
17
|
interface Extension {
|
|
17
18
|
id: string;
|
|
@@ -98,6 +99,7 @@ const mockExtensions: Extension[] = [
|
|
|
98
99
|
];
|
|
99
100
|
|
|
100
101
|
export function MarketplaceManager() {
|
|
102
|
+
const { t } = useTranslation();
|
|
101
103
|
const [extensions, setExtensions] = useState<Extension[]>(mockExtensions);
|
|
102
104
|
const [searchQuery, setSearchQuery] = useState("");
|
|
103
105
|
const [activeCategory, setActiveCategory] = useState("All");
|
|
@@ -152,7 +154,7 @@ export function MarketplaceManager() {
|
|
|
152
154
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity" />
|
|
153
155
|
<input
|
|
154
156
|
type="text"
|
|
155
|
-
placeholder="Search marketplace..."
|
|
157
|
+
placeholder={t("fields.searchMarketplace", { defaultValue: "Search marketplace..." })}
|
|
156
158
|
value={searchQuery}
|
|
157
159
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
158
160
|
className="pl-9 pr-4 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl text-xs font-bold w-64 focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all"
|
|
@@ -34,7 +34,9 @@ import {
|
|
|
34
34
|
withCacheBust,
|
|
35
35
|
apiUpload,
|
|
36
36
|
} from "../lib/api";
|
|
37
|
+
import { adminPath } from "../lib/paths";
|
|
37
38
|
import { useAuthStore, useUIStore, toast } from "../lib/stores";
|
|
39
|
+
import { useTranslation } from "react-i18next";
|
|
38
40
|
|
|
39
41
|
interface MediaItem {
|
|
40
42
|
id: string;
|
|
@@ -141,6 +143,7 @@ export function MediaGallery({
|
|
|
141
143
|
multiple?: boolean;
|
|
142
144
|
pickerMode?: boolean;
|
|
143
145
|
}) {
|
|
146
|
+
const { t } = useTranslation();
|
|
144
147
|
const { permissions } = useAuthStore();
|
|
145
148
|
const canUpload = permissions?.media?.create !== false;
|
|
146
149
|
const canDelete = permissions?.media?.delete !== false;
|
|
@@ -490,7 +493,7 @@ export function MediaGallery({
|
|
|
490
493
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
491
494
|
<input
|
|
492
495
|
type="text"
|
|
493
|
-
placeholder="Search assets..."
|
|
496
|
+
placeholder={t("fields.searchAssets", { defaultValue: "Search assets..." })}
|
|
494
497
|
value={search}
|
|
495
498
|
onChange={(e) => setSearch(e.target.value)}
|
|
496
499
|
className="w-full pl-10 pr-4 py-2.5 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all text-xs font-bold"
|
|
@@ -968,7 +971,7 @@ export function MediaGallery({
|
|
|
968
971
|
<SlidePanel
|
|
969
972
|
open={!!panelItem}
|
|
970
973
|
onClose={() => setPanelItem(null)}
|
|
971
|
-
title="Asset Details"
|
|
974
|
+
title={t("tooltips.assetDetails", { defaultValue: "Asset Details" })}
|
|
972
975
|
subtitle={panelItem?.filename}
|
|
973
976
|
>
|
|
974
977
|
{panelItem && (
|
|
@@ -1064,7 +1067,7 @@ export function MediaGallery({
|
|
|
1064
1067
|
updateMetadata(panelItem.id, { alt: e.target.value })
|
|
1065
1068
|
}
|
|
1066
1069
|
className="w-full px-4 py-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg text-xs font-bold transition-all resize-none min-h-[80px] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
1067
|
-
placeholder="Describe this asset..."
|
|
1070
|
+
placeholder={t("fields.describeThisAsset", { defaultValue: "Describe this asset..." })}
|
|
1068
1071
|
/>
|
|
1069
1072
|
</div>
|
|
1070
1073
|
<div>
|
|
@@ -1079,7 +1082,7 @@ export function MediaGallery({
|
|
|
1079
1082
|
})
|
|
1080
1083
|
}
|
|
1081
1084
|
className="w-full px-4 py-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg text-xs font-bold transition-all resize-none min-h-[80px] focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
1082
|
-
placeholder="Add a caption..."
|
|
1085
|
+
placeholder={t("fields.addACaption", { defaultValue: "Add a caption..." })}
|
|
1083
1086
|
/>
|
|
1084
1087
|
</div>
|
|
1085
1088
|
</div>
|
|
@@ -1119,7 +1122,7 @@ export function MediaGallery({
|
|
|
1119
1122
|
try {
|
|
1120
1123
|
await apiDelete(`/api/media/${panelItem.id}`);
|
|
1121
1124
|
setPanelItem(null);
|
|
1122
|
-
loadMedia();
|
|
1125
|
+
loadMedia(1);
|
|
1123
1126
|
} catch (error) {
|
|
1124
1127
|
console.error("Delete failed:", error);
|
|
1125
1128
|
toast.error("Failed to delete asset");
|
|
@@ -1213,15 +1216,15 @@ export function MediaGallery({
|
|
|
1213
1216
|
open={showNewFolderModal}
|
|
1214
1217
|
onClose={() => setShowNewFolderModal(false)}
|
|
1215
1218
|
onSubmit={createFolder}
|
|
1216
|
-
title="Create New Folder"
|
|
1217
|
-
placeholder="Folder name"
|
|
1219
|
+
title={t("tooltips.createNewFolder", { defaultValue: "Create New Folder" })}
|
|
1220
|
+
placeholder={t("fields.folderName", { defaultValue: "Folder name" })}
|
|
1218
1221
|
/>
|
|
1219
1222
|
)}
|
|
1220
1223
|
{!pickerMode && (
|
|
1221
1224
|
<Modal
|
|
1222
1225
|
open={showStorageConfigModal}
|
|
1223
1226
|
onClose={() => setShowStorageConfigModal(false)}
|
|
1224
|
-
title="Storage Not Configured"
|
|
1227
|
+
title={t("tooltips.storageNotConfigured", { defaultValue: "Storage Not Configured" })}
|
|
1225
1228
|
size="md"
|
|
1226
1229
|
>
|
|
1227
1230
|
<div className="text-center">
|
|
@@ -1235,7 +1238,7 @@ export function MediaGallery({
|
|
|
1235
1238
|
</p>
|
|
1236
1239
|
<div className="flex gap-3">
|
|
1237
1240
|
<a
|
|
1238
|
-
href=
|
|
1241
|
+
href={`${adminPath}/settings/storage-settings`}
|
|
1239
1242
|
className="flex-1 px-4 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-center hover:opacity-90 transition-colors"
|
|
1240
1243
|
>
|
|
1241
1244
|
Configure Storage
|