@rovela-ai/sdk 0.16.4 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/api/branding.d.ts +79 -0
- package/dist/admin/api/branding.d.ts.map +1 -0
- package/dist/admin/api/branding.js +238 -0
- package/dist/admin/api/branding.js.map +1 -0
- package/dist/admin/components/AdminTopBar.d.ts +1 -1
- package/dist/admin/components/AdminTopBar.d.ts.map +1 -1
- package/dist/admin/components/AdminTopBar.js +19 -2
- package/dist/admin/components/AdminTopBar.js.map +1 -1
- package/dist/admin/components/BrandingAssetUpload.d.ts +14 -0
- package/dist/admin/components/BrandingAssetUpload.d.ts.map +1 -0
- package/dist/admin/components/BrandingAssetUpload.js +207 -0
- package/dist/admin/components/BrandingAssetUpload.js.map +1 -0
- package/dist/admin/components/LogoUpload.d.ts +11 -10
- package/dist/admin/components/LogoUpload.d.ts.map +1 -1
- package/dist/admin/components/LogoUpload.js +8 -192
- package/dist/admin/components/LogoUpload.js.map +1 -1
- package/dist/admin/components/index.d.ts +2 -0
- package/dist/admin/components/index.d.ts.map +1 -1
- package/dist/admin/components/index.js +1 -0
- package/dist/admin/components/index.js.map +1 -1
- package/dist/admin/index.d.ts +2 -2
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/core/StoreSettingsProvider.d.ts +4 -0
- package/dist/core/StoreSettingsProvider.d.ts.map +1 -1
- package/dist/core/StoreSettingsProvider.js +3 -0
- package/dist/core/StoreSettingsProvider.js.map +1 -1
- package/dist/core/api/settings.d.ts +1 -0
- package/dist/core/api/settings.d.ts.map +1 -1
- package/dist/core/api/settings.js +2 -0
- package/dist/core/api/settings.js.map +1 -1
- package/dist/core/db/queries.d.ts +33 -31
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +11 -1
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/db/schema.d.ts +20 -1
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +3 -0
- package/dist/core/db/schema.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rovela-ai/sdk/admin/api/branding
|
|
3
|
+
*
|
|
4
|
+
* Store-side branding asset management (logo + favicon) — the store admin's
|
|
5
|
+
* half of the branding architecture shared with the Rovela platform.
|
|
6
|
+
*
|
|
7
|
+
* ARCHITECTURE (lockstep with the platform's lib/branding-asset.ts): the ONE
|
|
8
|
+
* source of truth is the R2 object at the fixed key
|
|
9
|
+
* `stores/{STORE_ID}/branding/{logo|favicon}` (no extension — R2 serves the
|
|
10
|
+
* Content-Type from object metadata, so the URL never changes when the file
|
|
11
|
+
* format changes). Everything else is a pointer that only encodes existence:
|
|
12
|
+
*
|
|
13
|
+
* - store_settings.logo_url / favicon_url — the store-side RUNTIME mirror.
|
|
14
|
+
* StoreSettingsProvider, the storefront header, transactional emails and
|
|
15
|
+
* the admin top bar all read settings-first, so writes here are live
|
|
16
|
+
* immediately (no redeploy).
|
|
17
|
+
* - projects.blueprint.store.{logoUrl|faviconUrl} — the PLATFORM-side
|
|
18
|
+
* mirror (written by the platform branding routes; feeds the env vars at
|
|
19
|
+
* deploy time). This handler cannot reach it, and doesn't need to: with
|
|
20
|
+
* settings-first readers the env vars are a build-time fallback only.
|
|
21
|
+
* - NEXT_PUBLIC_STORE_{LOGO|FAVICON}_URL — build-time fallback.
|
|
22
|
+
*
|
|
23
|
+
* Contract for every writer: update R2 + every mirror you can reach. This
|
|
24
|
+
* handler writes R2 + store_settings. GET reads the truth directly (an R2
|
|
25
|
+
* HEAD on the fixed key) so the admin UI can never lie about whether an
|
|
26
|
+
* asset exists, regardless of mirror state.
|
|
27
|
+
*/
|
|
28
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
29
|
+
export type BrandingKind = 'logo' | 'favicon';
|
|
30
|
+
interface BrandingApiError {
|
|
31
|
+
error: string;
|
|
32
|
+
code?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare const logoGET: () => Promise<NextResponse<BrandingApiError> | NextResponse<{
|
|
35
|
+
success: boolean;
|
|
36
|
+
data: {
|
|
37
|
+
exists: boolean;
|
|
38
|
+
url: string | null;
|
|
39
|
+
};
|
|
40
|
+
}>>;
|
|
41
|
+
export declare const logoPOST: (req: NextRequest) => Promise<NextResponse<BrandingApiError> | NextResponse<{
|
|
42
|
+
success: boolean;
|
|
43
|
+
data: {
|
|
44
|
+
url: string;
|
|
45
|
+
key: string;
|
|
46
|
+
size: number;
|
|
47
|
+
type: string;
|
|
48
|
+
settingsUpdated: boolean;
|
|
49
|
+
};
|
|
50
|
+
}>>;
|
|
51
|
+
export declare const logoDELETE: () => Promise<NextResponse<BrandingApiError> | NextResponse<{
|
|
52
|
+
success: boolean;
|
|
53
|
+
deleted: boolean;
|
|
54
|
+
settingsUpdated: boolean;
|
|
55
|
+
}>>;
|
|
56
|
+
export declare const faviconGET: () => Promise<NextResponse<BrandingApiError> | NextResponse<{
|
|
57
|
+
success: boolean;
|
|
58
|
+
data: {
|
|
59
|
+
exists: boolean;
|
|
60
|
+
url: string | null;
|
|
61
|
+
};
|
|
62
|
+
}>>;
|
|
63
|
+
export declare const faviconPOST: (req: NextRequest) => Promise<NextResponse<BrandingApiError> | NextResponse<{
|
|
64
|
+
success: boolean;
|
|
65
|
+
data: {
|
|
66
|
+
url: string;
|
|
67
|
+
key: string;
|
|
68
|
+
size: number;
|
|
69
|
+
type: string;
|
|
70
|
+
settingsUpdated: boolean;
|
|
71
|
+
};
|
|
72
|
+
}>>;
|
|
73
|
+
export declare const faviconDELETE: () => Promise<NextResponse<BrandingApiError> | NextResponse<{
|
|
74
|
+
success: boolean;
|
|
75
|
+
deleted: boolean;
|
|
76
|
+
settingsUpdated: boolean;
|
|
77
|
+
}>>;
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=branding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../../src/admin/api/branding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAcvD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,CAAA;AAE7C,UAAU,gBAAgB;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AA6QD,eAAO,MAAM,OAAO;;;;;;GAAmB,CAAA;AACvC,eAAO,MAAM,QAAQ,QA9MM,WAAW;;;;;;;;;GA8MG,CAAA;AACzC,eAAO,MAAM,UAAU;;;;GAAsB,CAAA;AAE7C,eAAO,MAAM,UAAU;;;;;;GAAsB,CAAA;AAC7C,eAAO,MAAM,WAAW,QAlNG,WAAW;;;;;;;;;GAkNS,CAAA;AAC/C,eAAO,MAAM,aAAa;;;;GAAyB,CAAA"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rovela-ai/sdk/admin/api/branding
|
|
3
|
+
*
|
|
4
|
+
* Store-side branding asset management (logo + favicon) — the store admin's
|
|
5
|
+
* half of the branding architecture shared with the Rovela platform.
|
|
6
|
+
*
|
|
7
|
+
* ARCHITECTURE (lockstep with the platform's lib/branding-asset.ts): the ONE
|
|
8
|
+
* source of truth is the R2 object at the fixed key
|
|
9
|
+
* `stores/{STORE_ID}/branding/{logo|favicon}` (no extension — R2 serves the
|
|
10
|
+
* Content-Type from object metadata, so the URL never changes when the file
|
|
11
|
+
* format changes). Everything else is a pointer that only encodes existence:
|
|
12
|
+
*
|
|
13
|
+
* - store_settings.logo_url / favicon_url — the store-side RUNTIME mirror.
|
|
14
|
+
* StoreSettingsProvider, the storefront header, transactional emails and
|
|
15
|
+
* the admin top bar all read settings-first, so writes here are live
|
|
16
|
+
* immediately (no redeploy).
|
|
17
|
+
* - projects.blueprint.store.{logoUrl|faviconUrl} — the PLATFORM-side
|
|
18
|
+
* mirror (written by the platform branding routes; feeds the env vars at
|
|
19
|
+
* deploy time). This handler cannot reach it, and doesn't need to: with
|
|
20
|
+
* settings-first readers the env vars are a build-time fallback only.
|
|
21
|
+
* - NEXT_PUBLIC_STORE_{LOGO|FAVICON}_URL — build-time fallback.
|
|
22
|
+
*
|
|
23
|
+
* Contract for every writer: update R2 + every mirror you can reach. This
|
|
24
|
+
* handler writes R2 + store_settings. GET reads the truth directly (an R2
|
|
25
|
+
* HEAD on the fixed key) so the admin UI can never lie about whether an
|
|
26
|
+
* asset exists, regardless of mirror state.
|
|
27
|
+
*/
|
|
28
|
+
import { NextResponse } from 'next/server';
|
|
29
|
+
import { PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, } from '@aws-sdk/client-s3';
|
|
30
|
+
import { requireAdmin } from '../server/admin-session';
|
|
31
|
+
import { getR2Client, getBucketName, getPublicUrlBase, getStoreId } from '../../media/server';
|
|
32
|
+
import { findSettings, upsertSettings, setOnboardingFlag } from '../../core/db/queries';
|
|
33
|
+
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
|
34
|
+
const BASE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/svg+xml'];
|
|
35
|
+
const KINDS = {
|
|
36
|
+
logo: {
|
|
37
|
+
settingsField: 'logoUrl',
|
|
38
|
+
envVar: 'NEXT_PUBLIC_STORE_LOGO_URL',
|
|
39
|
+
allowedTypes: BASE_TYPES,
|
|
40
|
+
label: 'Logo',
|
|
41
|
+
},
|
|
42
|
+
favicon: {
|
|
43
|
+
settingsField: 'faviconUrl',
|
|
44
|
+
envVar: 'NEXT_PUBLIC_STORE_FAVICON_URL',
|
|
45
|
+
// Favicons additionally accept .ico (both MIME spellings browsers emit).
|
|
46
|
+
allowedTypes: [...BASE_TYPES, 'image/x-icon', 'image/vnd.microsoft.icon'],
|
|
47
|
+
label: 'Favicon',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
/** Fixed R2 key — mirror of the platform's getBrandingR2Key. No extension. */
|
|
51
|
+
function brandingR2Key(storeId, kind) {
|
|
52
|
+
return `stores/${storeId}/branding/${kind}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Mirror the asset URL (or null on delete) into store_settings so every
|
|
56
|
+
* settings-first reader picks it up live. Awaited but non-fatal: R2 is the
|
|
57
|
+
* truth and GET reads R2 directly, so a mirror failure degrades to "shows
|
|
58
|
+
* after the next successful settings write" instead of failing the upload.
|
|
59
|
+
*/
|
|
60
|
+
async function mirrorToSettings(kind, url) {
|
|
61
|
+
try {
|
|
62
|
+
await upsertSettings({ [KINDS[kind].settingsField]: url });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error(`[Branding API] store_settings ${KINDS[kind].settingsField} mirror failed:`, err);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Handler factory
|
|
72
|
+
// =============================================================================
|
|
73
|
+
function createBrandingHandlers(kind) {
|
|
74
|
+
const cfg = KINDS[kind];
|
|
75
|
+
async function POST(req) {
|
|
76
|
+
try {
|
|
77
|
+
// Branding is a settings write — owner + administrator only.
|
|
78
|
+
const guard = await requireAdmin({ permission: 'settings.write' });
|
|
79
|
+
if (!guard.ok)
|
|
80
|
+
return guard.response;
|
|
81
|
+
let storeId;
|
|
82
|
+
let bucketName;
|
|
83
|
+
let publicUrl;
|
|
84
|
+
try {
|
|
85
|
+
storeId = getStoreId();
|
|
86
|
+
bucketName = getBucketName();
|
|
87
|
+
publicUrl = getPublicUrlBase();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return NextResponse.json({ error: 'Media storage not configured', code: 'NOT_CONFIGURED' }, { status: 503 });
|
|
91
|
+
}
|
|
92
|
+
let formData;
|
|
93
|
+
try {
|
|
94
|
+
formData = await req.formData();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return NextResponse.json({ error: 'Invalid form data', code: 'INVALID_REQUEST' }, { status: 400 });
|
|
98
|
+
}
|
|
99
|
+
// Field name matches the kind ('logo' / 'favicon'); accept 'file' as a
|
|
100
|
+
// generic fallback so future callers don't have to know the kind.
|
|
101
|
+
const file = (formData.get(kind) ?? formData.get('file'));
|
|
102
|
+
if (!file) {
|
|
103
|
+
return NextResponse.json({ error: `No ${kind} file provided`, code: 'MISSING_FILE' }, { status: 400 });
|
|
104
|
+
}
|
|
105
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
106
|
+
return NextResponse.json({ error: `${cfg.label} file exceeds maximum size of 2MB`, code: 'FILE_TOO_LARGE' }, { status: 400 });
|
|
107
|
+
}
|
|
108
|
+
if (!cfg.allowedTypes.includes(file.type)) {
|
|
109
|
+
return NextResponse.json({
|
|
110
|
+
error: `Invalid file type. Allowed: PNG, JPG, WebP, SVG${kind === 'favicon' ? ', ICO' : ''}`,
|
|
111
|
+
code: 'INVALID_TYPE',
|
|
112
|
+
}, { status: 400 });
|
|
113
|
+
}
|
|
114
|
+
const r2Key = brandingR2Key(storeId, kind);
|
|
115
|
+
try {
|
|
116
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
117
|
+
await getR2Client().send(new PutObjectCommand({
|
|
118
|
+
Bucket: bucketName,
|
|
119
|
+
Key: r2Key,
|
|
120
|
+
Body: Buffer.from(arrayBuffer),
|
|
121
|
+
ContentType: file.type, // R2 serves this back — the fixed URL never changes
|
|
122
|
+
// Explicit cache policy: the key is fixed, bytes get replaced in
|
|
123
|
+
// place. Without this, browsers heuristic-cache a long-unchanged
|
|
124
|
+
// asset for days; max-age=300 caps staleness at 5 minutes.
|
|
125
|
+
CacheControl: 'public, max-age=300',
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error(`[Branding API] Failed to upload ${kind}:`, err);
|
|
130
|
+
return NextResponse.json({ error: `Failed to upload ${kind}`, code: 'UPLOAD_FAILED' }, { status: 500 });
|
|
131
|
+
}
|
|
132
|
+
const url = `${publicUrl}/${r2Key}`;
|
|
133
|
+
const settingsUpdated = await mirrorToSettings(kind, url);
|
|
134
|
+
if (kind === 'logo') {
|
|
135
|
+
// Setup-guide writeback: mark "Upload your own logo" complete.
|
|
136
|
+
// Fire-and-forget — onboarding flag failures never block the upload.
|
|
137
|
+
setOnboardingFlag('logoUploaded', true).catch((err) => {
|
|
138
|
+
console.error('[Branding API] onboarding writeback failed:', err);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
console.log(`[Branding API] Uploaded ${kind} for store ${storeId}: ${r2Key} (${file.size} bytes, ${file.type}, settings mirror: ${settingsUpdated})`);
|
|
142
|
+
return NextResponse.json({
|
|
143
|
+
success: true,
|
|
144
|
+
data: { url, key: r2Key, size: file.size, type: file.type, settingsUpdated },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error(`[Branding API] ${kind} POST error:`, error);
|
|
149
|
+
return NextResponse.json({ error: `Failed to process ${kind} upload`, code: 'INTERNAL_ERROR' }, { status: 500 });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function DELETE() {
|
|
153
|
+
try {
|
|
154
|
+
const guard = await requireAdmin({ permission: 'settings.write' });
|
|
155
|
+
if (!guard.ok)
|
|
156
|
+
return guard.response;
|
|
157
|
+
let storeId;
|
|
158
|
+
let bucketName;
|
|
159
|
+
try {
|
|
160
|
+
storeId = getStoreId();
|
|
161
|
+
bucketName = getBucketName();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return NextResponse.json({ error: 'Media storage not configured', code: 'NOT_CONFIGURED' }, { status: 503 });
|
|
165
|
+
}
|
|
166
|
+
const r2Key = brandingR2Key(storeId, kind);
|
|
167
|
+
let deleted = false;
|
|
168
|
+
try {
|
|
169
|
+
await getR2Client().send(new DeleteObjectCommand({ Bucket: bucketName, Key: r2Key }));
|
|
170
|
+
deleted = true;
|
|
171
|
+
console.log(`[Branding API] Deleted ${kind}: ${r2Key}`);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Object didn't exist — that's fine, the mirror clear below still runs.
|
|
175
|
+
console.log(`[Branding API] No ${kind} found to delete: ${r2Key}`);
|
|
176
|
+
}
|
|
177
|
+
const settingsUpdated = await mirrorToSettings(kind, null);
|
|
178
|
+
return NextResponse.json({ success: true, deleted, settingsUpdated });
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error(`[Branding API] ${kind} DELETE error:`, error);
|
|
182
|
+
return NextResponse.json({ error: `Failed to delete ${kind}`, code: 'INTERNAL_ERROR' }, { status: 500 });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function GET() {
|
|
186
|
+
try {
|
|
187
|
+
// Every authenticated admin can read (the UI renders the asset).
|
|
188
|
+
const guard = await requireAdmin({ permission: 'settings.read' });
|
|
189
|
+
if (!guard.ok)
|
|
190
|
+
return guard.response;
|
|
191
|
+
// Truth first: HEAD the fixed R2 key. The object either exists or it
|
|
192
|
+
// doesn't — no mirror (env var, settings row, blueprint) can lie here.
|
|
193
|
+
try {
|
|
194
|
+
const storeId = getStoreId();
|
|
195
|
+
const bucketName = getBucketName();
|
|
196
|
+
const publicUrl = getPublicUrlBase();
|
|
197
|
+
const r2Key = brandingR2Key(storeId, kind);
|
|
198
|
+
let exists = false;
|
|
199
|
+
try {
|
|
200
|
+
await getR2Client().send(new HeadObjectCommand({ Bucket: bucketName, Key: r2Key }));
|
|
201
|
+
exists = true;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
exists = false; // 404 / NotFound — asset absent
|
|
205
|
+
}
|
|
206
|
+
return NextResponse.json({
|
|
207
|
+
success: true,
|
|
208
|
+
data: { exists, url: exists ? `${publicUrl}/${r2Key}` : null },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// R2 not configured — degrade to the pointer chain (settings → env).
|
|
213
|
+
const settings = await findSettings().catch(() => null);
|
|
214
|
+
const url = settings?.[cfg.settingsField] ||
|
|
215
|
+
process.env[cfg.envVar] ||
|
|
216
|
+
null;
|
|
217
|
+
return NextResponse.json({ success: true, data: { exists: !!url, url } });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
console.error(`[Branding API] ${kind} GET error:`, error);
|
|
222
|
+
return NextResponse.json({ error: `Failed to check ${kind}`, code: 'INTERNAL_ERROR' }, { status: 500 });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { GET, POST, DELETE };
|
|
226
|
+
}
|
|
227
|
+
// =============================================================================
|
|
228
|
+
// Exported handlers — template routes re-export these
|
|
229
|
+
// =============================================================================
|
|
230
|
+
const logoHandlers = createBrandingHandlers('logo');
|
|
231
|
+
const faviconHandlers = createBrandingHandlers('favicon');
|
|
232
|
+
export const logoGET = logoHandlers.GET;
|
|
233
|
+
export const logoPOST = logoHandlers.POST;
|
|
234
|
+
export const logoDELETE = logoHandlers.DELETE;
|
|
235
|
+
export const faviconGET = faviconHandlers.GET;
|
|
236
|
+
export const faviconPOST = faviconHandlers.POST;
|
|
237
|
+
export const faviconDELETE = faviconHandlers.DELETE;
|
|
238
|
+
//# sourceMappingURL=branding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branding.js","sourceRoot":"","sources":["../../../src/admin/api/branding.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminTopBar.d.ts","sourceRoot":"","sources":["../../../src/admin/components/AdminTopBar.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"AdminTopBar.d.ts","sourceRoot":"","sources":["../../../src/admin/components/AdminTopBar.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAQ3D,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAMD,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EAAE,aAAuB,EAClC,IAAI,EACJ,QAAc,EACd,SAAc,GACf,EAAE,gBAAgB,2CA8FlB"}
|
|
@@ -17,6 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
*/
|
|
18
18
|
import Link from 'next/link';
|
|
19
19
|
import { ExternalLink, Pencil } from 'lucide-react';
|
|
20
|
+
import { useEffect, useState } from 'react';
|
|
20
21
|
import { useStoreSettingsOptional } from '../../core';
|
|
21
22
|
import { AdminUserMenu } from './AdminUserMenu';
|
|
22
23
|
// =============================================================================
|
|
@@ -29,7 +30,23 @@ export function AdminTopBar({ storeName: storeNameProp = 'Admin', logo, storeUrl
|
|
|
29
30
|
const storeSettings = useStoreSettingsOptional();
|
|
30
31
|
const settingsLoading = storeNameProp === 'Admin' && !!storeSettings?.isLoading && !storeSettings?.settings;
|
|
31
32
|
const storeName = storeNameProp !== 'Admin' ? storeNameProp : (storeSettings?.storeName || storeNameProp);
|
|
33
|
+
// Brand tile mark: favicon FIRST (a square mark belongs in a 32px tile;
|
|
34
|
+
// wide logo lockups read cramped), then logo, then monogram. Each source
|
|
35
|
+
// is settings-first (live store_settings via the provider) with the
|
|
36
|
+
// build-time env var as fallback — the branding write contract keeps the
|
|
37
|
+
// settings mirror fresh, so store-side changes show without a redeploy.
|
|
38
|
+
const faviconUrl = storeSettings?.faviconUrl || process.env.NEXT_PUBLIC_STORE_FAVICON_URL || null;
|
|
32
39
|
const logoUrl = storeSettings?.logoUrl || process.env.NEXT_PUBLIC_STORE_LOGO_URL || null;
|
|
40
|
+
const tileUrl = faviconUrl || logoUrl;
|
|
41
|
+
// A stale pointer (e.g. asset deleted store-side while an env var still
|
|
42
|
+
// carries the fixed URL) must degrade to the monogram, never a broken img.
|
|
43
|
+
const [tileBroken, setTileBroken] = useState(false);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
// Pattern A — reset internal state on prop change (retry when the URL changes).
|
|
46
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
47
|
+
setTileBroken(false);
|
|
48
|
+
}, [tileUrl]);
|
|
49
|
+
const tileSrc = tileBroken ? null : tileUrl;
|
|
33
50
|
// Deep-link back to the Rovela editor (/generate/[id]) so the merchant has
|
|
34
51
|
// a clean Manage ↔ Edit loop. Only rendered when NEXT_PUBLIC_ROVELA_PROJECT_ID
|
|
35
52
|
// is set (present on every store rebuilt since buildStoreEnvVars started
|
|
@@ -41,8 +58,8 @@ export function AdminTopBar({ storeName: storeNameProp = 'Admin', logo, storeUrl
|
|
|
41
58
|
const editStoreUrl = rovelaProjectId
|
|
42
59
|
? `${rovelaBase.replace(/\/$/, '')}/generate/${rovelaProjectId}`
|
|
43
60
|
: null;
|
|
44
|
-
return (_jsxs("header", { className: `admin-topbar ${className}`.trim(), children: [_jsxs(Link, { href: "/admin", className: "admin-topbar-brand", children: [logo || (_jsx("span", { className: `admin-brand-tile ${
|
|
61
|
+
return (_jsxs("header", { className: `admin-topbar ${className}`.trim(), children: [_jsxs(Link, { href: "/admin", className: "admin-topbar-brand", children: [logo || (_jsx("span", { className: `admin-brand-tile ${tileSrc ? '' : 'admin-brand-tile-monogram'}`, children: tileSrc ? (
|
|
45
62
|
// eslint-disable-next-line @next/next/no-img-element
|
|
46
|
-
_jsx("img", { src:
|
|
63
|
+
_jsx("img", { src: tileSrc, alt: "", onError: () => setTileBroken(true) })) : settingsLoading ? (_jsx("span", { className: "admin-skeleton admin-brand-tile-skeleton" })) : (storeName.charAt(0).toUpperCase()) })), settingsLoading ? (_jsx("span", { className: "admin-skeleton admin-topbar-name-skeleton" })) : (_jsx("span", { className: "admin-topbar-store", children: storeName })), _jsx("span", { className: "admin-topbar-tag", children: "Admin" })] }), _jsxs("div", { className: "admin-topbar-actions", children: [_jsxs("a", { href: storeUrl, target: "_blank", rel: "noopener noreferrer", className: "admin-topbar-btn", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), "View store"] }), editStoreUrl && (_jsxs("a", { href: editStoreUrl, target: "_blank", rel: "noopener noreferrer", className: "admin-topbar-btn", children: [_jsx(Pencil, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), "Edit store"] })), _jsx("span", { className: "admin-topbar-divider", "aria-hidden": "true" }), _jsx(AdminUserMenu, { compact: true })] })] }));
|
|
47
64
|
}
|
|
48
65
|
//# sourceMappingURL=AdminTopBar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminTopBar.js","sourceRoot":"","sources":["../../../src/admin/components/AdminTopBar.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;;;;;;;;;;;GAcG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"AdminTopBar.js","sourceRoot":"","sources":["../../../src/admin/components/AdminTopBar.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface BrandingAssetUploadProps {
|
|
2
|
+
/** Callback when the asset is successfully uploaded */
|
|
3
|
+
onUpload?: (url: string) => void;
|
|
4
|
+
/** Callback when the asset is successfully deleted */
|
|
5
|
+
onDelete?: () => void;
|
|
6
|
+
/** Additional CSS classes */
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function BrandingAssetUpload({ kind, onUpload, onDelete, className, }: BrandingAssetUploadProps & {
|
|
10
|
+
kind: 'logo' | 'favicon';
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export type FaviconUploadProps = BrandingAssetUploadProps;
|
|
13
|
+
export declare function FaviconUpload(props: FaviconUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=BrandingAssetUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrandingAssetUpload.d.ts","sourceRoot":"","sources":["../../../src/admin/components/BrandingAssetUpload.tsx"],"names":[],"mappings":"AAuBA,MAAM,WAAW,wBAAwB;IACvC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA0DD,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,SAAc,GACf,EAAE,wBAAwB,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,2CAuTzD;AAMD,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,CAAA;AAEzD,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,2CAEtD"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* @rovela/sdk/admin/components/BrandingAssetUpload
|
|
5
|
+
*
|
|
6
|
+
* Kind-parameterized branding asset upload (logo + favicon) for the admin
|
|
7
|
+
* settings page. `LogoUpload` (LogoUpload.tsx) and `FaviconUpload` (below)
|
|
8
|
+
* are thin instantiations — one implementation, two assets, zero drift.
|
|
9
|
+
*
|
|
10
|
+
* Talks to /api/admin/branding/{kind} (SDK handlers — R2 fixed key +
|
|
11
|
+
* store_settings mirror + R2-HEAD GET, see admin/api/branding.ts). The
|
|
12
|
+
* client-side `?v=` cache-buster on previews covers the fixed-URL byte
|
|
13
|
+
* replacement within this surface; the server sets max-age=300 for
|
|
14
|
+
* everything else.
|
|
15
|
+
*/
|
|
16
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
17
|
+
import { fetchAdminApi } from '../hooks/fetchAdminApi';
|
|
18
|
+
const BASE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/svg+xml'];
|
|
19
|
+
const BASE_ACCEPT = 'image/png,image/jpeg,image/jpg,image/webp,image/svg+xml';
|
|
20
|
+
const KIND_CONFIG = {
|
|
21
|
+
logo: {
|
|
22
|
+
endpoint: '/api/admin/branding/logo',
|
|
23
|
+
formField: 'logo',
|
|
24
|
+
label: 'Store Logo',
|
|
25
|
+
helper: 'Your logo appears in the header and emails. Recommended: 200x80px.',
|
|
26
|
+
allowedTypes: BASE_TYPES,
|
|
27
|
+
accept: BASE_ACCEPT,
|
|
28
|
+
invalidTypeMessage: 'Invalid file type. Use PNG, JPG, WebP, or SVG.',
|
|
29
|
+
preview: 'wide',
|
|
30
|
+
},
|
|
31
|
+
favicon: {
|
|
32
|
+
endpoint: '/api/admin/branding/favicon',
|
|
33
|
+
formField: 'favicon',
|
|
34
|
+
label: 'Favicon',
|
|
35
|
+
helper: 'The small square icon in the browser tab and the admin top bar. Recommended: a square PNG or SVG, at least 64x64px. Your logo is used when no favicon is set.',
|
|
36
|
+
allowedTypes: [...BASE_TYPES, 'image/x-icon', 'image/vnd.microsoft.icon'],
|
|
37
|
+
accept: `${BASE_ACCEPT},image/x-icon,image/vnd.microsoft.icon,.ico`,
|
|
38
|
+
invalidTypeMessage: 'Invalid file type. Use PNG, JPG, WebP, SVG, or ICO.',
|
|
39
|
+
preview: 'tile',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const MAX_SIZE = 2 * 1024 * 1024; // 2MB (mirrored server-side)
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Component
|
|
45
|
+
// =============================================================================
|
|
46
|
+
export function BrandingAssetUpload({ kind, onUpload, onDelete, className = '', }) {
|
|
47
|
+
const cfg = KIND_CONFIG[kind];
|
|
48
|
+
const [asset, setAsset] = useState({
|
|
49
|
+
exists: false,
|
|
50
|
+
url: null,
|
|
51
|
+
isLoading: true,
|
|
52
|
+
isUploading: false,
|
|
53
|
+
isDeleting: false,
|
|
54
|
+
error: null,
|
|
55
|
+
});
|
|
56
|
+
const [preview, setPreview] = useState(null);
|
|
57
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
58
|
+
const [successMessage, setSuccessMessage] = useState(null);
|
|
59
|
+
const [cacheBuster, setCacheBuster] = useState(() => Date.now());
|
|
60
|
+
const fileInputRef = useRef(null);
|
|
61
|
+
// Fetch status on mount — the GET is an R2 HEAD, so this reflects the
|
|
62
|
+
// actual object, not a possibly-stale env var or settings row.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
let cancelled = false;
|
|
65
|
+
async function fetchStatus() {
|
|
66
|
+
const res = await fetchAdminApi(cfg.endpoint);
|
|
67
|
+
if (cancelled)
|
|
68
|
+
return;
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
setAsset((prev) => ({ ...prev, isLoading: false, error: res.error }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setAsset({
|
|
74
|
+
exists: res.data.data.exists,
|
|
75
|
+
url: res.data.data.url,
|
|
76
|
+
isLoading: false,
|
|
77
|
+
isUploading: false,
|
|
78
|
+
isDeleting: false,
|
|
79
|
+
error: null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
fetchStatus();
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true;
|
|
85
|
+
};
|
|
86
|
+
}, [cfg.endpoint]);
|
|
87
|
+
const uploadAsset = useCallback(async (file) => {
|
|
88
|
+
setAsset((prev) => ({ ...prev, isUploading: true, error: null }));
|
|
89
|
+
const formData = new FormData();
|
|
90
|
+
formData.append(cfg.formField, file);
|
|
91
|
+
const res = await fetchAdminApi(cfg.endpoint, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: formData,
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
setAsset((prev) => ({ ...prev, isUploading: false, error: res.error }));
|
|
97
|
+
setPreview(null);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
setCacheBuster(Date.now());
|
|
101
|
+
setAsset({
|
|
102
|
+
exists: true,
|
|
103
|
+
url: res.data.data.url,
|
|
104
|
+
isLoading: false,
|
|
105
|
+
isUploading: false,
|
|
106
|
+
isDeleting: false,
|
|
107
|
+
error: null,
|
|
108
|
+
});
|
|
109
|
+
setPreview(null);
|
|
110
|
+
setSuccessMessage(`${cfg.label} uploaded successfully!`);
|
|
111
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
112
|
+
onUpload?.(res.data.data.url);
|
|
113
|
+
}, [cfg.endpoint, cfg.formField, cfg.label, onUpload]);
|
|
114
|
+
const handleFileSelect = useCallback((file) => {
|
|
115
|
+
setAsset((prev) => ({ ...prev, error: null }));
|
|
116
|
+
setSuccessMessage(null);
|
|
117
|
+
if (!cfg.allowedTypes.includes(file.type)) {
|
|
118
|
+
setAsset((prev) => ({ ...prev, error: cfg.invalidTypeMessage }));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (file.size > MAX_SIZE) {
|
|
122
|
+
setAsset((prev) => ({ ...prev, error: 'File too large. Maximum size is 2MB.' }));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const reader = new FileReader();
|
|
126
|
+
reader.onload = (e) => {
|
|
127
|
+
setPreview(e.target?.result);
|
|
128
|
+
};
|
|
129
|
+
reader.readAsDataURL(file);
|
|
130
|
+
uploadAsset(file);
|
|
131
|
+
}, [cfg.allowedTypes, cfg.invalidTypeMessage, uploadAsset]);
|
|
132
|
+
const handleDelete = async () => {
|
|
133
|
+
if (!asset.exists)
|
|
134
|
+
return;
|
|
135
|
+
setAsset((prev) => ({ ...prev, isDeleting: true, error: null }));
|
|
136
|
+
setSuccessMessage(null);
|
|
137
|
+
const res = await fetchAdminApi(cfg.endpoint, { method: 'DELETE' });
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
setAsset((prev) => ({ ...prev, isDeleting: false, error: res.error }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
setAsset({
|
|
143
|
+
exists: false,
|
|
144
|
+
url: null,
|
|
145
|
+
isLoading: false,
|
|
146
|
+
isUploading: false,
|
|
147
|
+
isDeleting: false,
|
|
148
|
+
error: null,
|
|
149
|
+
});
|
|
150
|
+
setSuccessMessage(`${cfg.label} removed successfully!`);
|
|
151
|
+
setTimeout(() => setSuccessMessage(null), 3000);
|
|
152
|
+
onDelete?.();
|
|
153
|
+
};
|
|
154
|
+
const handleFileInputChange = (e) => {
|
|
155
|
+
const file = e.target.files?.[0];
|
|
156
|
+
if (file) {
|
|
157
|
+
handleFileSelect(file);
|
|
158
|
+
}
|
|
159
|
+
if (fileInputRef.current) {
|
|
160
|
+
fileInputRef.current.value = '';
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const handleDragOver = (e) => {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
setIsDragging(true);
|
|
166
|
+
};
|
|
167
|
+
const handleDragLeave = (e) => {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
setIsDragging(false);
|
|
170
|
+
};
|
|
171
|
+
const handleDrop = (e) => {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
setIsDragging(false);
|
|
174
|
+
const file = e.dataTransfer.files?.[0];
|
|
175
|
+
if (file) {
|
|
176
|
+
handleFileSelect(file);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const getDisplayUrl = () => {
|
|
180
|
+
if (preview)
|
|
181
|
+
return preview;
|
|
182
|
+
if (asset.url)
|
|
183
|
+
return `${asset.url}?v=${cacheBuster}`;
|
|
184
|
+
return null;
|
|
185
|
+
};
|
|
186
|
+
const displayUrl = getDisplayUrl();
|
|
187
|
+
const isWorking = asset.isLoading || asset.isUploading || asset.isDeleting;
|
|
188
|
+
return (_jsxs("div", { className: className, children: [_jsx("label", { className: "admin-label", children: cfg.label }), _jsx("p", { className: "admin-helper-text mb-3", children: cfg.helper }), asset.error && (_jsx("div", { className: "admin-alert admin-alert-error mb-3", children: _jsx("div", { className: "admin-alert-description text-sm", children: asset.error }) })), successMessage && (_jsx("div", { className: "admin-alert admin-alert-success mb-3", children: _jsx("div", { className: "admin-alert-description text-sm", children: successMessage }) })), displayUrl ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "relative rounded-lg border border-[hsl(var(--admin-border))] bg-[hsl(var(--admin-background-subtle))] p-4", children: [_jsx("div", { className: cfg.preview === 'tile'
|
|
189
|
+
? 'flex items-center justify-center min-h-[56px]'
|
|
190
|
+
: 'flex items-center justify-center min-h-[80px]', children: _jsx("img", { src: displayUrl, alt: cfg.label, className: cfg.preview === 'tile'
|
|
191
|
+
? 'h-12 w-12 rounded-md object-contain'
|
|
192
|
+
: 'max-h-[80px] max-w-full object-contain' }) }), asset.isUploading && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-[hsl(var(--admin-background))]/80 rounded-lg", children: _jsxs("div", { className: "flex items-center gap-2 text-sm admin-text-muted", children: [_jsx("div", { className: "admin-spinner h-4 w-4" }), "Uploading..."] }) }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { ref: fileInputRef, type: "file", accept: cfg.accept, onChange: handleFileInputChange, className: "hidden" }), _jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: isWorking, className: "admin-btn admin-btn-secondary admin-btn-sm disabled:opacity-50", children: "Replace" }), _jsx("button", { type: "button", onClick: handleDelete, disabled: isWorking, className: "admin-btn admin-btn-destructive admin-btn-sm disabled:opacity-50", children: asset.isDeleting ? 'Removing...' : 'Remove' })] })] })) : (_jsxs("div", { onClick: () => !isWorking && fileInputRef.current?.click(), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, className: `
|
|
193
|
+
relative flex min-h-[120px] cursor-pointer flex-col items-center justify-center
|
|
194
|
+
rounded-lg border-2 border-dashed transition-all
|
|
195
|
+
${isDragging
|
|
196
|
+
? 'border-[hsl(var(--admin-primary))] bg-[hsl(var(--admin-primary)/0.05)]'
|
|
197
|
+
: 'border-[hsl(var(--admin-border))] hover:border-[hsl(var(--admin-primary)/0.5)]'}
|
|
198
|
+
${isWorking ? 'opacity-50 cursor-not-allowed' : ''}
|
|
199
|
+
`, children: [_jsx("input", { ref: fileInputRef, type: "file", accept: cfg.accept, onChange: handleFileInputChange, className: "hidden" }), asset.isLoading ? (_jsxs("div", { className: "flex items-center gap-2 admin-text-muted", children: [_jsx("div", { className: "admin-spinner h-5 w-5" }), _jsx("span", { className: "text-sm", children: "Checking..." })] })) : (_jsxs("div", { className: "flex flex-col items-center gap-2 p-4 text-center", children: [_jsx("div", { className: `
|
|
200
|
+
flex h-10 w-10 items-center justify-center rounded-full transition-colors
|
|
201
|
+
${isDragging ? 'bg-[hsl(var(--admin-primary)/0.2)]' : 'bg-[hsl(var(--admin-background-subtle))]'}
|
|
202
|
+
`, children: _jsx("svg", { className: `h-5 w-5 transition-colors ${isDragging ? 'text-[hsl(var(--admin-primary))]' : 'admin-icon-muted'}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }) }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium admin-text", children: isDragging ? 'Drop here' : 'Click or drag to upload' }), _jsx("p", { className: "admin-helper-text mt-1", children: kind === 'favicon' ? 'PNG, JPG, WebP, SVG, ICO (max 2MB)' : 'PNG, JPG, WebP, SVG (max 2MB)' })] })] }))] }))] }));
|
|
203
|
+
}
|
|
204
|
+
export function FaviconUpload(props) {
|
|
205
|
+
return _jsx(BrandingAssetUpload, { kind: "favicon", ...props });
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=BrandingAssetUpload.js.map
|