Skip to content

Commit 2532fc6

Browse files
author
minhnq
committed
Merge remote-tracking branch 'origin/main'
# Conflicts: # src/app/json/page.tsx
2 parents 4b2b715 + 839d352 commit 2532fc6

3 files changed

Lines changed: 522 additions & 51 deletions

File tree

src/app/j/[id]/route.ts

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,77 @@ const corsHeaders = {
99
'Content-Type': 'application/json',
1010
};
1111

12+
const MAX_DELAY_MS = 30000; // Max 30 seconds
13+
14+
// Parse delay value: 2000, "2s", "500ms"
15+
function parseDelay(value: string | null): number {
16+
if (!value) return 0;
17+
18+
const trimmed = value.trim().toLowerCase();
19+
20+
// Parse with suffix
21+
const match = trimmed.match(/^(\d+(?:\.\d+)?)(ms|s)?$/);
22+
if (!match) return 0;
23+
24+
const num = parseFloat(match[1]);
25+
const unit = match[2] || 'ms';
26+
27+
let ms: number;
28+
if (unit === 's') {
29+
ms = num * 1000;
30+
} else {
31+
ms = num;
32+
}
33+
34+
return Math.min(Math.max(0, Math.round(ms)), MAX_DELAY_MS);
35+
}
36+
37+
// Pick specific keys from object
38+
function pickKeys(obj: unknown, keys: string[]): unknown {
39+
if (typeof obj !== 'object' || obj === null) return obj;
40+
if (Array.isArray(obj)) {
41+
return obj.map(item => pickKeys(item, keys));
42+
}
43+
44+
const result: Record<string, unknown> = {};
45+
for (const key of keys) {
46+
if (key in (obj as Record<string, unknown>)) {
47+
result[key] = (obj as Record<string, unknown>)[key];
48+
}
49+
}
50+
return result;
51+
}
52+
53+
// Simple JSONPath implementation (just basic path like "$.users[0].name")
54+
function jsonPath(obj: unknown, path: string): unknown {
55+
if (!path.startsWith('$.')) return obj;
56+
57+
const parts = path.slice(2).split(/\.|\[|\]/).filter(Boolean);
58+
let current: unknown = obj;
59+
60+
for (const part of parts) {
61+
if (current === null || current === undefined) return undefined;
62+
63+
if (typeof current === 'object') {
64+
if (Array.isArray(current)) {
65+
const index = parseInt(part, 10);
66+
if (!isNaN(index)) {
67+
current = current[index];
68+
} else {
69+
// Arrays don't have string keys in this context
70+
return undefined;
71+
}
72+
} else {
73+
current = (current as Record<string, unknown>)[part];
74+
}
75+
} else {
76+
return undefined;
77+
}
78+
}
79+
80+
return current;
81+
}
82+
1283
export async function OPTIONS() {
1384
return new NextResponse(null, {
1485
status: 204,
@@ -22,13 +93,24 @@ export async function GET(
2293
) {
2394
try {
2495
const { id } = await params;
96+
const searchParams = request.nextUrl.searchParams;
97+
98+
// Parse query params
99+
const delay = parseDelay(searchParams.get('delay'));
100+
const pathQuery = searchParams.get('path');
101+
const pickQuery = searchParams.get('pick');
102+
103+
// Apply delay if specified
104+
if (delay > 0) {
105+
await new Promise(resolve => setTimeout(resolve, delay));
106+
}
25107

26108
const result = await sql`
27-
SELECT content, expires_at
28-
FROM json_bins
29-
WHERE id = ${id}
30-
AND (expires_at IS NULL OR expires_at > NOW())
31-
`;
109+
SELECT content, expires_at
110+
FROM json_bins
111+
WHERE id = ${id}
112+
AND (expires_at IS NULL OR expires_at > NOW())
113+
`;
32114

33115
if (result.length === 0) {
34116
return NextResponse.json(
@@ -37,10 +119,40 @@ export async function GET(
37119
);
38120
}
39121

40-
// Return raw JSON content
41-
return new NextResponse(result[0].content, {
122+
let content = result[0].content;
123+
let parsed: unknown;
124+
125+
// Apply transformations if requested
126+
if (pathQuery || pickQuery) {
127+
try {
128+
parsed = JSON.parse(content);
129+
130+
// Apply JSONPath
131+
if (pathQuery) {
132+
parsed = jsonPath(parsed, pathQuery);
133+
}
134+
135+
// Apply pick keys
136+
if (pickQuery) {
137+
const keys = pickQuery.split(',').map(k => k.trim()).filter(Boolean);
138+
if (keys.length > 0) {
139+
parsed = pickKeys(parsed, keys);
140+
}
141+
}
142+
143+
content = JSON.stringify(parsed);
144+
} catch {
145+
// If parsing fails, return original content
146+
}
147+
}
148+
149+
// Return JSON content
150+
return new NextResponse(content, {
42151
status: 200,
43-
headers: corsHeaders,
152+
headers: {
153+
...corsHeaders,
154+
'X-Delay-Applied': delay > 0 ? `${delay}ms` : '0',
155+
},
44156
});
45157
} catch (error) {
46158
console.error('Error fetching JSON:', error);

src/app/json/page.module.css

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,193 @@
432432
color: var(--text-muted);
433433
}
434434

435+
/* Edit Mode Bar */
436+
.editModeBar {
437+
display: flex;
438+
align-items: center;
439+
gap: 12px;
440+
padding: 12px 16px;
441+
background: var(--neo-cyan);
442+
border: 3px solid var(--neo-black);
443+
margin-bottom: 16px;
444+
font-size: 14px;
445+
font-weight: 600;
446+
}
447+
448+
.editModeBar code {
449+
font-family: 'Consolas', monospace;
450+
background: var(--neo-white);
451+
padding: 2px 8px;
452+
border: 2px solid var(--neo-black);
453+
}
454+
455+
.exitEditButton {
456+
margin-left: auto;
457+
font-size: 12px;
458+
font-weight: 600;
459+
padding: 6px 12px;
460+
background: var(--neo-white);
461+
border: 2px solid var(--neo-black);
462+
cursor: pointer;
463+
transition: all 0.1s ease;
464+
}
465+
466+
.exitEditButton:hover {
467+
background: var(--neo-yellow);
468+
}
469+
470+
/* Action Buttons */
471+
.actionButtons {
472+
display: flex;
473+
gap: 12px;
474+
}
475+
476+
.updateButton {
477+
flex: 2;
478+
font-family: 'Lexend Mega', sans-serif;
479+
font-size: 14px;
480+
font-weight: 800;
481+
text-transform: uppercase;
482+
padding: 16px;
483+
background: var(--neo-cyan);
484+
color: var(--neo-black);
485+
border: 4px solid var(--neo-black);
486+
box-shadow: 6px 6px 0 var(--neo-black);
487+
cursor: pointer;
488+
transition: all 0.1s ease;
489+
}
490+
491+
.updateButton:hover:not(:disabled) {
492+
transform: translate(-2px, -2px);
493+
box-shadow: 8px 8px 0 var(--neo-black);
494+
}
495+
496+
.updateButton:disabled {
497+
opacity: 0.5;
498+
cursor: not-allowed;
499+
}
500+
501+
.saveNewButton {
502+
flex: 1;
503+
font-family: 'Lexend Mega', sans-serif;
504+
font-size: 12px;
505+
font-weight: 700;
506+
text-transform: uppercase;
507+
padding: 16px;
508+
background: var(--neo-white);
509+
color: var(--neo-black);
510+
border: 4px solid var(--neo-black);
511+
box-shadow: 4px 4px 0 var(--neo-black);
512+
cursor: pointer;
513+
transition: all 0.1s ease;
514+
}
515+
516+
.saveNewButton:hover:not(:disabled) {
517+
background: var(--neo-green);
518+
}
519+
520+
.saveNewButton:disabled {
521+
opacity: 0.5;
522+
cursor: not-allowed;
523+
}
524+
525+
/* Code Snippets */
526+
.snippetsSection {
527+
margin-top: 16px;
528+
border-top: 2px dashed var(--neo-black);
529+
padding-top: 16px;
530+
}
531+
532+
.snippetsToggle {
533+
font-family: 'Public Sans', sans-serif;
534+
font-size: 14px;
535+
font-weight: 600;
536+
background: none;
537+
border: none;
538+
cursor: pointer;
539+
color: var(--text-muted);
540+
padding: 0;
541+
}
542+
543+
.snippetsToggle:hover {
544+
color: var(--neo-black);
545+
}
546+
547+
.snippetsContent {
548+
margin-top: 12px;
549+
}
550+
551+
.snippetTabs {
552+
display: flex;
553+
gap: 4px;
554+
margin-bottom: 8px;
555+
}
556+
557+
.snippetTab {
558+
font-family: 'Public Sans', sans-serif;
559+
font-size: 12px;
560+
font-weight: 600;
561+
padding: 6px 12px;
562+
background: var(--neo-white);
563+
border: 2px solid var(--neo-black);
564+
cursor: pointer;
565+
transition: all 0.1s ease;
566+
}
567+
568+
.snippetTab:hover {
569+
background: var(--neo-pink);
570+
color: var(--neo-white);
571+
}
572+
573+
.snippetTab.activeTab {
574+
background: var(--neo-black);
575+
color: var(--neo-white);
576+
}
577+
578+
.snippetCode {
579+
position: relative;
580+
background: #1a1a1a;
581+
border: 3px solid var(--neo-black);
582+
padding: 12px;
583+
}
584+
585+
.snippetCode pre {
586+
font-family: 'Consolas', monospace;
587+
font-size: 12px;
588+
line-height: 1.5;
589+
color: #00ff00;
590+
margin: 0;
591+
overflow-x: auto;
592+
white-space: pre-wrap;
593+
word-break: break-all;
594+
}
595+
596+
.snippetCopy {
597+
position: absolute;
598+
top: 8px;
599+
right: 8px;
600+
display: flex;
601+
align-items: center;
602+
justify-content: center;
603+
width: 28px;
604+
height: 28px;
605+
background: var(--neo-cyan);
606+
border: 2px solid var(--neo-black);
607+
cursor: pointer;
608+
}
609+
610+
.snippetCopy:hover {
611+
transform: scale(1.1);
612+
}
613+
614+
/* Feature Icon SVG fix */
615+
.featureIcon {
616+
display: flex;
617+
align-items: center;
618+
justify-content: center;
619+
color: var(--neo-black);
620+
}
621+
435622
/* Responsive */
436623
@media (max-width: 768px) {
437624
.header {
@@ -468,4 +655,12 @@
468655
width: 100%;
469656
margin-bottom: 8px;
470657
}
658+
659+
.actionButtons {
660+
flex-direction: column;
661+
}
662+
663+
.editModeBar {
664+
flex-wrap: wrap;
665+
}
471666
}

0 commit comments

Comments
 (0)