@jhits/plugin-content 0.0.15 → 0.0.18
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/api/files.d.ts +27 -0
- package/dist/api/files.d.ts.map +1 -0
- package/dist/api/files.js +159 -0
- package/dist/api/handler.d.ts +4 -0
- package/dist/api/handler.d.ts.map +1 -1
- package/dist/api/links.d.ts +23 -0
- package/dist/api/links.d.ts.map +1 -0
- package/dist/api/links.js +75 -0
- package/dist/api/router.d.ts +6 -0
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +32 -4
- package/dist/components/DynamicLink.d.ts +28 -0
- package/dist/components/DynamicLink.d.ts.map +1 -0
- package/dist/components/DynamicLink.js +62 -0
- package/dist/components/LinkSettingsModal.d.ts +22 -0
- package/dist/components/LinkSettingsModal.d.ts.map +1 -0
- package/dist/components/LinkSettingsModal.js +172 -0
- package/dist/components/TranslationEditor.d.ts +3 -1
- package/dist/components/TranslationEditor.d.ts.map +1 -1
- package/dist/components/TranslationEditor.js +11 -5
- package/dist/hooks/useLinks.d.ts +23 -0
- package/dist/hooks/useLinks.d.ts.map +1 -0
- package/dist/hooks/useLinks.js +56 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -8
- package/dist/views/LinkManager/LinkManager.d.ts +9 -0
- package/dist/views/LinkManager/LinkManager.d.ts.map +1 -0
- package/dist/views/LinkManager/LinkManager.js +90 -0
- package/dist/views/MediaManager/MediaManager.d.ts +8 -0
- package/dist/views/MediaManager/MediaManager.d.ts.map +1 -0
- package/dist/views/MediaManager/MediaManager.js +93 -0
- package/dist/views/index.d.ts +10 -0
- package/dist/views/index.d.ts.map +1 -0
- package/dist/views/index.js +22 -0
- package/package.json +1 -1
- package/src/api/files.ts +192 -0
- package/src/api/handler.ts +2 -0
- package/src/api/links.ts +107 -0
- package/src/api/router.ts +37 -8
- package/src/components/DynamicLink.tsx +152 -0
- package/src/components/LinkSettingsModal.tsx +442 -0
- package/src/components/TranslationEditor.tsx +18 -7
- package/src/hooks/useLinks.ts +75 -0
- package/src/index.tsx +40 -9
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files API Handler
|
|
3
|
+
* Handles uploading and managing general files
|
|
4
|
+
*/
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
export interface FilesApiConfig {
|
|
7
|
+
getDb: () => Promise<{
|
|
8
|
+
db: () => any;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* GET /api/plugin-content/files - List all files
|
|
13
|
+
*/
|
|
14
|
+
export declare function GET(req: NextRequest, config: FilesApiConfig): Promise<NextResponse>;
|
|
15
|
+
/**
|
|
16
|
+
* POST /api/plugin-content/files/upload - Upload a file
|
|
17
|
+
*/
|
|
18
|
+
export declare function UPLOAD(req: NextRequest, config: FilesApiConfig): Promise<NextResponse>;
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/plugin-content/files/download - Download a file
|
|
21
|
+
*/
|
|
22
|
+
export declare function DOWNLOAD(req: NextRequest, config: FilesApiConfig): Promise<NextResponse>;
|
|
23
|
+
/**
|
|
24
|
+
* DELETE /api/plugin-content/files - Delete a file
|
|
25
|
+
*/
|
|
26
|
+
export declare function DELETE(req: NextRequest, config: FilesApiConfig): Promise<NextResponse>;
|
|
27
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/api/files.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKxD,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;CAC3C;AAYD;;GAEG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAmBzF;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAmD5F;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAyC9F;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAsC5F"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files API Handler
|
|
3
|
+
* Handles uploading and managing general files
|
|
4
|
+
*/
|
|
5
|
+
import { NextResponse } from 'next/server';
|
|
6
|
+
import { writeFile, mkdir, unlink, readFile } from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { randomBytes } from 'crypto';
|
|
9
|
+
const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
|
|
10
|
+
async function ensureUploadsDir() {
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(uploadsDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
// Directory might already exist
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* GET /api/plugin-content/files - List all files
|
|
20
|
+
*/
|
|
21
|
+
export async function GET(req, config) {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(req.url);
|
|
24
|
+
const siteId = url.searchParams.get('siteId') || 'default';
|
|
25
|
+
const dbConnection = await config.getDb();
|
|
26
|
+
const db = dbConnection.db();
|
|
27
|
+
const files = db.collection('files');
|
|
28
|
+
const data = await files.find({ siteId }).sort({ uploadedAt: -1 }).toArray();
|
|
29
|
+
return NextResponse.json({ files: data });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error('[FilesAPI] GET error:', err);
|
|
33
|
+
return NextResponse.json({ error: 'Failed to fetch files', detail: err.message }, { status: 500 });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* POST /api/plugin-content/files/upload - Upload a file
|
|
38
|
+
*/
|
|
39
|
+
export async function UPLOAD(req, config) {
|
|
40
|
+
try {
|
|
41
|
+
await ensureUploadsDir();
|
|
42
|
+
const formData = await req.formData();
|
|
43
|
+
const file = formData.get('file');
|
|
44
|
+
const siteId = formData.get('siteId') || 'default';
|
|
45
|
+
if (!file) {
|
|
46
|
+
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
|
47
|
+
}
|
|
48
|
+
// Generate unique filename
|
|
49
|
+
const ext = path.extname(file.name);
|
|
50
|
+
const uniqueId = randomBytes(16).toString('hex');
|
|
51
|
+
const timestamp = Date.now();
|
|
52
|
+
const uniqueFilename = `${timestamp}-${uniqueId}${ext}`;
|
|
53
|
+
const filePath = path.join(uploadsDir, uniqueFilename);
|
|
54
|
+
// Save file
|
|
55
|
+
const bytes = await file.arrayBuffer();
|
|
56
|
+
const buffer = Buffer.from(bytes);
|
|
57
|
+
await writeFile(filePath, buffer);
|
|
58
|
+
const fileMetadata = {
|
|
59
|
+
id: uniqueFilename,
|
|
60
|
+
filename: file.name,
|
|
61
|
+
url: `/api/plugin-content/files/download?id=${uniqueFilename}`, // Point to our own download endpoint
|
|
62
|
+
size: file.size,
|
|
63
|
+
mimeType: file.type,
|
|
64
|
+
siteId,
|
|
65
|
+
uploadedAt: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
const dbConnection = await config.getDb();
|
|
68
|
+
const db = dbConnection.db();
|
|
69
|
+
const files = db.collection('files');
|
|
70
|
+
await files.insertOne(fileMetadata);
|
|
71
|
+
return NextResponse.json({
|
|
72
|
+
success: true,
|
|
73
|
+
file: fileMetadata,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error('[FilesAPI] UPLOAD error:', err);
|
|
78
|
+
return NextResponse.json({ error: 'Failed to upload file', detail: err.message }, { status: 500 });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* GET /api/plugin-content/files/download - Download a file
|
|
83
|
+
*/
|
|
84
|
+
export async function DOWNLOAD(req, config) {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(req.url);
|
|
87
|
+
const id = url.searchParams.get('id');
|
|
88
|
+
const download = url.searchParams.get('download') === 'true';
|
|
89
|
+
if (!id) {
|
|
90
|
+
return NextResponse.json({ error: 'ID is required' }, { status: 400 });
|
|
91
|
+
}
|
|
92
|
+
// Security: Prevent directory traversal
|
|
93
|
+
const sanitizedFilename = path.basename(id);
|
|
94
|
+
const filePath = path.join(uploadsDir, sanitizedFilename);
|
|
95
|
+
const fileBuffer = await readFile(filePath);
|
|
96
|
+
// Determine content type
|
|
97
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
98
|
+
let contentType = 'application/octet-stream';
|
|
99
|
+
if (ext === '.pdf')
|
|
100
|
+
contentType = 'application/pdf';
|
|
101
|
+
else if (ext === '.png')
|
|
102
|
+
contentType = 'image/png';
|
|
103
|
+
else if (ext === '.jpg' || ext === '.jpeg')
|
|
104
|
+
contentType = 'image/jpeg';
|
|
105
|
+
else if (ext === '.webp')
|
|
106
|
+
contentType = 'image/webp';
|
|
107
|
+
else if (ext === '.svg')
|
|
108
|
+
contentType = 'image/svg+xml';
|
|
109
|
+
const headers = {
|
|
110
|
+
'Content-Type': contentType,
|
|
111
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
112
|
+
};
|
|
113
|
+
if (download) {
|
|
114
|
+
headers['Content-Disposition'] = `attachment; filename="${sanitizedFilename}"`;
|
|
115
|
+
}
|
|
116
|
+
return new NextResponse(fileBuffer, {
|
|
117
|
+
headers: headers,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error('[FilesAPI] DOWNLOAD error:', err);
|
|
122
|
+
return new NextResponse('File not found', { status: 404 });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* DELETE /api/plugin-content/files - Delete a file
|
|
127
|
+
*/
|
|
128
|
+
export async function DELETE(req, config) {
|
|
129
|
+
try {
|
|
130
|
+
const url = new URL(req.url);
|
|
131
|
+
const siteId = url.searchParams.get('siteId') || 'default';
|
|
132
|
+
const id = url.searchParams.get('id');
|
|
133
|
+
if (!id) {
|
|
134
|
+
return NextResponse.json({ error: 'ID is required' }, { status: 400 });
|
|
135
|
+
}
|
|
136
|
+
const dbConnection = await config.getDb();
|
|
137
|
+
const db = dbConnection.db();
|
|
138
|
+
const files = db.collection('files');
|
|
139
|
+
const fileDoc = await files.findOne({ siteId, id });
|
|
140
|
+
if (!fileDoc) {
|
|
141
|
+
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
|
142
|
+
}
|
|
143
|
+
// Delete from filesystem
|
|
144
|
+
const filePath = path.join(uploadsDir, fileDoc.id);
|
|
145
|
+
try {
|
|
146
|
+
await unlink(filePath);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
console.warn(`[FilesAPI] File could not be deleted from disk: ${filePath}`, err);
|
|
150
|
+
}
|
|
151
|
+
// Delete from database
|
|
152
|
+
await files.deleteOne({ siteId, id });
|
|
153
|
+
return NextResponse.json({ success: true });
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
console.error('[FilesAPI] DELETE error:', err);
|
|
157
|
+
return NextResponse.json({ error: 'Failed to delete file', detail: err.message }, { status: 500 });
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/api/handler.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
10
10
|
export interface ContentApiConfig {
|
|
11
11
|
/** Directory where locale files are stored (default: 'data/locales') */
|
|
12
12
|
localesDir?: string;
|
|
13
|
+
/** MongoDB client promise */
|
|
14
|
+
getDb?: () => Promise<{
|
|
15
|
+
db: () => any;
|
|
16
|
+
}>;
|
|
13
17
|
}
|
|
14
18
|
/**
|
|
15
19
|
* POST /api/plugin-content/save - Save translations
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,MAAM,WAAW,gBAAgB;IAC7B,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,MAAM,WAAW,gBAAgB;IAC7B,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkD5F"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links API Handler
|
|
3
|
+
* Handles global, localized links and buttons
|
|
4
|
+
*/
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
export interface LinksApiConfig {
|
|
7
|
+
getDb: () => Promise<{
|
|
8
|
+
db: () => any;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* GET /api/plugin-content/links - List all links
|
|
13
|
+
*/
|
|
14
|
+
export declare function GET(req: NextRequest, config: LinksApiConfig): Promise<NextResponse>;
|
|
15
|
+
/**
|
|
16
|
+
* POST /api/plugin-content/links - Save a link
|
|
17
|
+
*/
|
|
18
|
+
export declare function POST(req: NextRequest, config: LinksApiConfig): Promise<NextResponse>;
|
|
19
|
+
/**
|
|
20
|
+
* DELETE /api/plugin-content/links - Delete a link
|
|
21
|
+
*/
|
|
22
|
+
export declare function DELETE(req: NextRequest, config: LinksApiConfig): Promise<NextResponse>;
|
|
23
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/api/links.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;CAC3C;AAED;;GAEG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAmBzF;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAuC1F;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAwB5F"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links API Handler
|
|
3
|
+
* Handles global, localized links and buttons
|
|
4
|
+
*/
|
|
5
|
+
import { NextResponse } from 'next/server';
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/plugin-content/links - List all links
|
|
8
|
+
*/
|
|
9
|
+
export async function GET(req, config) {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(req.url);
|
|
12
|
+
const siteId = url.searchParams.get('siteId') || 'default';
|
|
13
|
+
const dbConnection = await config.getDb();
|
|
14
|
+
const db = dbConnection.db();
|
|
15
|
+
const links = db.collection('links');
|
|
16
|
+
const data = await links.find({ siteId }).toArray();
|
|
17
|
+
return NextResponse.json({ links: data });
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
console.error('[LinksAPI] GET error:', err);
|
|
21
|
+
return NextResponse.json({ error: 'Failed to fetch links', detail: err.message }, { status: 500 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* POST /api/plugin-content/links - Save a link
|
|
26
|
+
*/
|
|
27
|
+
export async function POST(req, config) {
|
|
28
|
+
try {
|
|
29
|
+
const body = await req.json();
|
|
30
|
+
const { siteId = 'default', link } = body;
|
|
31
|
+
if (!link || !link.key) {
|
|
32
|
+
return NextResponse.json({ error: 'Link key is required' }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
const dbConnection = await config.getDb();
|
|
35
|
+
const db = dbConnection.db();
|
|
36
|
+
const links = db.collection('links');
|
|
37
|
+
// Update or insert
|
|
38
|
+
const updatedLink = {
|
|
39
|
+
...link,
|
|
40
|
+
siteId,
|
|
41
|
+
updatedAt: new Date(),
|
|
42
|
+
};
|
|
43
|
+
// Remove _id from link before update to avoid immutable field error if it's a string
|
|
44
|
+
const { _id, ...linkData } = updatedLink;
|
|
45
|
+
await links.updateOne({ siteId, key: link.key }, { $set: linkData }, { upsert: true });
|
|
46
|
+
const savedLink = await links.findOne({ siteId, key: link.key });
|
|
47
|
+
return NextResponse.json({ success: true, link: savedLink });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error('[LinksAPI] POST error:', err);
|
|
51
|
+
return NextResponse.json({ error: 'Failed to save link', detail: err.message }, { status: 500 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* DELETE /api/plugin-content/links - Delete a link
|
|
56
|
+
*/
|
|
57
|
+
export async function DELETE(req, config) {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(req.url);
|
|
60
|
+
const siteId = url.searchParams.get('siteId') || 'default';
|
|
61
|
+
const key = url.searchParams.get('key');
|
|
62
|
+
if (!key) {
|
|
63
|
+
return NextResponse.json({ error: 'Key is required' }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
const dbConnection = await config.getDb();
|
|
66
|
+
const db = dbConnection.db();
|
|
67
|
+
const links = db.collection('links');
|
|
68
|
+
await links.deleteOne({ siteId, key });
|
|
69
|
+
return NextResponse.json({ success: true });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error('[LinksAPI] DELETE error:', err);
|
|
73
|
+
return NextResponse.json({ error: 'Failed to delete link', detail: err.message }, { status: 500 });
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/api/router.d.ts
CHANGED
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { NextRequest, NextResponse } from 'next/server';
|
|
9
9
|
export interface ContentApiRouterConfig {
|
|
10
|
+
/** MongoDB client promise - should return { db: () => Database } */
|
|
11
|
+
getDb: () => Promise<{
|
|
12
|
+
db: () => any;
|
|
13
|
+
}>;
|
|
10
14
|
/** Directory where locale files are stored (default: 'data/locales') */
|
|
11
15
|
localesDir?: string;
|
|
16
|
+
/** Site ID for multi-site setups */
|
|
17
|
+
siteId?: string;
|
|
12
18
|
}
|
|
13
19
|
/**
|
|
14
20
|
* Handle content API requests
|
package/dist/api/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD,MAAM,WAAW,sBAAsB;IACnC,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD,MAAM,WAAW,sBAAsB;IACnC,oEAAoE;IACpE,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;IACxC,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAClC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,sBAAsB,GAC/B,OAAO,CAAC,YAAY,CAAC,CA6DvB"}
|
package/dist/api/router.js
CHANGED
|
@@ -16,17 +16,45 @@ export async function handleContentApi(req, path, config) {
|
|
|
16
16
|
const method = req.method;
|
|
17
17
|
const safePath = Array.isArray(path) ? path : [];
|
|
18
18
|
const route = safePath.length > 0 ? safePath[0] : '';
|
|
19
|
-
console.log(`[ContentApiRouter] method=${method}, path=${JSON.stringify(safePath)}, route=${route}, url=${req.url}`);
|
|
20
19
|
try {
|
|
21
20
|
// Route: /api/plugin-content/save
|
|
22
21
|
if (route === 'save') {
|
|
23
22
|
if (method === 'POST') {
|
|
24
|
-
return await SaveHandler(req, config);
|
|
23
|
+
return await SaveHandler(req, { localesDir: config.localesDir });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Route: /api/plugin-content/links
|
|
27
|
+
else if (route === 'links') {
|
|
28
|
+
const linksModule = await import('./links');
|
|
29
|
+
if (method === 'GET') {
|
|
30
|
+
return await linksModule.GET(req, { getDb: config.getDb });
|
|
31
|
+
}
|
|
32
|
+
if (method === 'POST') {
|
|
33
|
+
return await linksModule.POST(req, { getDb: config.getDb });
|
|
34
|
+
}
|
|
35
|
+
if (method === 'DELETE') {
|
|
36
|
+
return await linksModule.DELETE(req, { getDb: config.getDb });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Route: /api/plugin-content/files
|
|
40
|
+
else if (route === 'files') {
|
|
41
|
+
const filesModule = await import('./files');
|
|
42
|
+
const subRoute = safePath.length > 1 ? safePath[1] : '';
|
|
43
|
+
if (subRoute === 'upload' && method === 'POST') {
|
|
44
|
+
return await filesModule.UPLOAD(req, { getDb: config.getDb });
|
|
45
|
+
}
|
|
46
|
+
if (subRoute === 'download' && method === 'GET') {
|
|
47
|
+
return await filesModule.DOWNLOAD(req, { getDb: config.getDb });
|
|
48
|
+
}
|
|
49
|
+
if (method === 'GET') {
|
|
50
|
+
return await filesModule.GET(req, { getDb: config.getDb });
|
|
51
|
+
}
|
|
52
|
+
if (method === 'DELETE') {
|
|
53
|
+
return await filesModule.DELETE(req, { getDb: config.getDb });
|
|
25
54
|
}
|
|
26
|
-
return NextResponse.json({ error: `Method ${method} not allowed for route: save` }, { status: 405 });
|
|
27
55
|
}
|
|
28
56
|
// Route not found
|
|
29
|
-
return NextResponse.json({ error: `Route not found: ${route || '/'}` }, { status: 404 });
|
|
57
|
+
return NextResponse.json({ error: `Route not found: ${route || '/'}${safePath.length > 1 ? '/' + safePath[1] : ''}` }, { status: 404 });
|
|
30
58
|
}
|
|
31
59
|
catch (error) {
|
|
32
60
|
console.error('[ContentApiRouter] Error:', error);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Link Component
|
|
3
|
+
* Localized link with right-click settings for admins
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
interface DynamicLinkProps {
|
|
7
|
+
linkKey: string;
|
|
8
|
+
siteId?: string;
|
|
9
|
+
locale: string;
|
|
10
|
+
defaultLabel: string;
|
|
11
|
+
defaultTarget: string;
|
|
12
|
+
defaultType?: 'url' | 'file';
|
|
13
|
+
className?: string;
|
|
14
|
+
isAdmin?: boolean;
|
|
15
|
+
apiBaseUrl?: string;
|
|
16
|
+
/** Whether to use a button element instead of <a>/Link (useful for scroll actions) */
|
|
17
|
+
isButton?: boolean;
|
|
18
|
+
/** Click handler for button type */
|
|
19
|
+
onClick?: (target: string) => void;
|
|
20
|
+
children?: (data: {
|
|
21
|
+
label: string;
|
|
22
|
+
target: string;
|
|
23
|
+
type: 'url' | 'file';
|
|
24
|
+
}) => React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
export declare function DynamicLink({ linkKey, siteId, locale, defaultLabel, defaultTarget, defaultType, className, isAdmin, apiBaseUrl, isButton, onClick, children }: DynamicLinkProps): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=DynamicLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DynamicLink.d.ts","sourceRoot":"","sources":["../../src/components/DynamicLink.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAKnD,UAAU,gBAAgB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oCAAoC;IACpC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,KAAK,KAAK,CAAC,SAAS,CAAC;CACjG;AAED,wBAAgB,WAAW,CAAC,EACxB,OAAO,EACP,MAAkB,EAClB,MAAM,EACN,YAAY,EACZ,aAAa,EACb,WAAmB,EACnB,SAAc,EACd,OAAe,EACf,UAAU,EACV,QAAgB,EAChB,OAAO,EACP,QAAQ,EACX,EAAE,gBAAgB,2CA6GlB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Link Component
|
|
3
|
+
* Localized link with right-click settings for admins
|
|
4
|
+
*/
|
|
5
|
+
'use client';
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { useLinks } from '../hooks/useLinks';
|
|
9
|
+
import { LinkSettingsModal } from './LinkSettingsModal';
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
export function DynamicLink({ linkKey, siteId = 'default', locale, defaultLabel, defaultTarget, defaultType = 'url', className = '', isAdmin = false, apiBaseUrl, isButton = false, onClick, children }) {
|
|
12
|
+
const { getLink, refetch } = useLinks(siteId, apiBaseUrl);
|
|
13
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
14
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
15
|
+
const linkData = getLink(linkKey, locale) || {
|
|
16
|
+
label: defaultLabel,
|
|
17
|
+
target: defaultTarget,
|
|
18
|
+
type: defaultType
|
|
19
|
+
};
|
|
20
|
+
const handleContextMenu = (e) => {
|
|
21
|
+
if (isAdmin) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
setIsModalOpen(true);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const handleSaveSuccess = () => {
|
|
28
|
+
if (refetch)
|
|
29
|
+
refetch();
|
|
30
|
+
};
|
|
31
|
+
const isExternal = linkData.target.startsWith('http') || linkData.type === 'file';
|
|
32
|
+
const isScroll = linkData.target.startsWith('#');
|
|
33
|
+
// Render content
|
|
34
|
+
const content = children ? children(linkData) : linkData.label;
|
|
35
|
+
// Use a wrapper that is invisible to layout when not admin
|
|
36
|
+
// When admin, we need relative for the HUD tooltip
|
|
37
|
+
// We also pass through the className to the wrapper if admin to preserve flex/grid item behavior
|
|
38
|
+
const wrapperClass = isAdmin ? `relative ${className}` : 'contents';
|
|
39
|
+
// When not admin, the className is on the link itself
|
|
40
|
+
// When admin, the wrapper has the main classes, and the link is just a transparent overlay
|
|
41
|
+
const linkClass = isAdmin ? "w-full h-full flex items-center justify-center bg-transparent border-none p-0 m-0 text-inherit font-inherit cursor-pointer outline-none" : className;
|
|
42
|
+
const renderLink = () => {
|
|
43
|
+
if (isScroll || isButton) {
|
|
44
|
+
return (_jsx("button", { type: "button", onClick: (e) => {
|
|
45
|
+
if (isScroll) {
|
|
46
|
+
const targetId = linkData.target.replace('#', '');
|
|
47
|
+
const element = document.getElementById(targetId);
|
|
48
|
+
if (element) {
|
|
49
|
+
element.scrollIntoView({ behavior: 'smooth' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (onClick)
|
|
53
|
+
onClick(linkData.target);
|
|
54
|
+
}, className: linkClass, children: content }));
|
|
55
|
+
}
|
|
56
|
+
if (isExternal) {
|
|
57
|
+
return (_jsx("a", { href: linkData.target, target: "_blank", rel: "noopener noreferrer", className: linkClass, children: content }));
|
|
58
|
+
}
|
|
59
|
+
return (_jsx(Link, { href: linkData.target, className: linkClass, children: content }));
|
|
60
|
+
};
|
|
61
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { onContextMenu: handleContextMenu, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), className: wrapperClass, children: [isAdmin && isHovered && (_jsx("div", { className: "absolute -top-8 left-1/2 -translate-x-1/2 bg-primary text-white text-[8px] font-black uppercase px-2 py-1 rounded shadow-xl z-[1000] pointer-events-none whitespace-nowrap", children: "Right-click to edit link" })), renderLink()] }), _jsx(LinkSettingsModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), linkKey: linkKey, siteId: siteId, locale: locale, initialData: linkData, onSaveSuccess: handleSaveSuccess, apiBaseUrl: apiBaseUrl })] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Settings Modal
|
|
3
|
+
* Inline editor for dynamic links
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
interface LinkSettingsModalProps {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
linkKey: string;
|
|
10
|
+
siteId: string;
|
|
11
|
+
locale: string;
|
|
12
|
+
initialData?: {
|
|
13
|
+
label: string;
|
|
14
|
+
target: string;
|
|
15
|
+
type: 'url' | 'file';
|
|
16
|
+
};
|
|
17
|
+
onSaveSuccess?: () => void;
|
|
18
|
+
apiBaseUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function LinkSettingsModal({ isOpen, onClose, linkKey, siteId, locale, initialData, onSaveSuccess, apiBaseUrl }: LinkSettingsModalProps): React.ReactPortal | null;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=LinkSettingsModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LinkSettingsModal.d.ts","sourceRoot":"","sources":["../../src/components/LinkSettingsModal.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAcnD,UAAU,sBAAsB;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;KACxB,CAAC;IACF,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,iBAAiB,CAAC,EAC9B,MAAM,EACN,OAAO,EACP,OAAO,EACP,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,UAAkC,EACrC,EAAE,sBAAsB,4BA4YxB"}
|