Skip to content

Commit 4493489

Browse files
committed
style: homogenize header/footer and replace slate with gray
- Footer: GitHub icon + text · coffee emoji + text (consistent with other apps) - Header: consistent layout with favicon, title, tagline, privacy badge - Replace all slate-* colors with gray-* for consistency - Add bg-gray-50/dark:bg-gray-900 to app root
1 parent d12ab07 commit 4493489

15 files changed

Lines changed: 255 additions & 51 deletions

File tree

src/App.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect, useCallback } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { useDarkMode } from './hooks/useDarkMode';
44
import Header from './components/Header';
@@ -8,6 +8,7 @@ import DiffView from './components/DiffView';
88
import Footer from './components/Footer';
99
import { formatJson, validateJson, jsonToYaml } from './utils/json';
1010
import { useBeforeUnload } from './hooks/useBeforeUnload';
11+
import { useFileDrop } from './hooks/useFileDrop';
1112

1213
type Mode = 'editor' | 'diff';
1314

@@ -22,6 +23,27 @@ export default function App() {
2223

2324
useBeforeUnload(input.length > 0);
2425

26+
// Prevent browser default file open on window drop
27+
useEffect(() => {
28+
function preventDrop(e: DragEvent) {
29+
e.preventDefault();
30+
}
31+
window.addEventListener('dragover', preventDrop);
32+
window.addEventListener('drop', preventDrop);
33+
return () => {
34+
window.removeEventListener('dragover', preventDrop);
35+
window.removeEventListener('drop', preventDrop);
36+
};
37+
}, []);
38+
39+
// Global drop fallback — populate main editor input
40+
const handleGlobalFileDrop = useCallback((content: string) => {
41+
setInput(content);
42+
if (mode === 'diff') setMode('editor');
43+
}, [mode]);
44+
45+
const { isDragging: isGlobalDragging, dropError: globalDropError, onDragOver: globalDragOver, onDragLeave: globalDragLeave, onDrop: globalDrop } = useFileDrop(handleGlobalFileDrop);
46+
2547
function handleFormat() {
2648
const { output: o, error: e } = formatJson(input);
2749
setOutput(o);
@@ -47,7 +69,12 @@ export default function App() {
4769
}
4870

4971
return (
50-
<div className="min-h-screen flex flex-col">
72+
<div
73+
className="min-h-screen flex flex-col relative bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors"
74+
onDragOver={globalDragOver}
75+
onDragLeave={globalDragLeave}
76+
onDrop={globalDrop}
77+
>
5178
<Header dark={dark} toggleDark={toggleDark} />
5279
<Toolbar
5380
mode={mode}
@@ -71,6 +98,21 @@ export default function App() {
7198
)}
7299
</main>
73100
<Footer />
101+
{isGlobalDragging && (
102+
<div
103+
className="fixed inset-0 flex items-center justify-center bg-indigo-500/5 dark:bg-indigo-400/5 pointer-events-none z-50"
104+
aria-label={t('dropFileHere')}
105+
>
106+
<span className="text-indigo-600 dark:text-indigo-400 font-semibold text-lg bg-white/80 dark:bg-gray-900/80 px-6 py-3 rounded-xl shadow-lg">
107+
{t('dropFileHere')}
108+
</span>
109+
</div>
110+
)}
111+
{globalDropError && (
112+
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 text-red-600 dark:text-red-400 text-sm bg-red-50 dark:bg-red-900/20 px-4 py-2 rounded-lg shadow">
113+
{globalDropError}
114+
</div>
115+
)}
74116
</div>
75117
);
76118
}

src/components/DiffView.tsx

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,76 @@
11
import { useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { diffJson, diffJsonLines } from '../utils/json';
4+
import { useFileDrop } from '../hooks/useFileDrop';
45

56
type DiffMode = 'keys' | 'lines';
67

8+
function DiffTextarea({
9+
value,
10+
onChange,
11+
placeholder,
12+
ariaLabel,
13+
borderColor,
14+
}: {
15+
value: string;
16+
onChange: (v: string) => void;
17+
placeholder: string;
18+
ariaLabel: string;
19+
borderColor: 'indigo' | 'amber';
20+
}) {
21+
const { t } = useTranslation();
22+
const { isDragging, dropError, onDragOver, onDragLeave, onDrop } = useFileDrop(onChange);
23+
24+
const dragBorder = borderColor === 'indigo'
25+
? 'border-indigo-500 dark:border-indigo-400 border-2'
26+
: 'border-amber-500 dark:border-amber-400 border-2';
27+
28+
const overlayBg = borderColor === 'indigo'
29+
? 'bg-indigo-500/10 dark:bg-indigo-400/10'
30+
: 'bg-amber-500/10 dark:bg-amber-400/10';
31+
32+
const overlayText = borderColor === 'indigo'
33+
? 'text-indigo-600 dark:text-indigo-400'
34+
: 'text-amber-600 dark:text-amber-400';
35+
36+
return (
37+
<div className="flex-1 flex flex-col gap-1">
38+
<div
39+
className="relative flex-1"
40+
onDragOver={onDragOver}
41+
onDragLeave={onDragLeave}
42+
onDrop={onDrop}
43+
>
44+
<textarea
45+
value={value}
46+
onChange={(e) => onChange(e.target.value)}
47+
placeholder={placeholder}
48+
aria-label={ariaLabel}
49+
className={`flex-1 min-h-[200px] w-full h-full p-3 rounded-lg border bg-white dark:bg-gray-800 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-amber-500 ${
50+
isDragging ? dragBorder : 'border-gray-300 dark:border-gray-600'
51+
}`}
52+
spellCheck={false}
53+
/>
54+
{isDragging && (
55+
<div
56+
className={`absolute inset-0 flex items-center justify-center rounded-lg ${overlayBg} pointer-events-none`}
57+
aria-label={t('dropFileHere')}
58+
>
59+
<span className={`${overlayText} font-medium text-sm`}>
60+
{t('dropFileHere')}
61+
</span>
62+
</div>
63+
)}
64+
</div>
65+
{dropError && (
66+
<div className="text-red-600 dark:text-red-400 text-xs bg-red-50 dark:bg-red-900/20 p-1.5 rounded">
67+
{dropError}
68+
</div>
69+
)}
70+
</div>
71+
);
72+
}
73+
774
export default function DiffView() {
875
const { t } = useTranslation();
976
const [left, setLeft] = useState('');
@@ -42,21 +109,19 @@ export default function DiffView() {
42109
return (
43110
<div className="flex flex-col gap-4">
44111
<div className="flex flex-col lg:flex-row gap-4">
45-
<textarea
112+
<DiffTextarea
46113
value={left}
47-
onChange={(e) => setLeft(e.target.value)}
114+
onChange={setLeft}
48115
placeholder={t('diffLeftPlaceholder')}
49-
aria-label={t('diffLeftPlaceholder')}
50-
className="flex-1 min-h-[200px] p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-800 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-amber-500"
51-
spellCheck={false}
116+
ariaLabel={t('diffLeftPlaceholder')}
117+
borderColor="indigo"
52118
/>
53-
<textarea
119+
<DiffTextarea
54120
value={right}
55-
onChange={(e) => setRight(e.target.value)}
121+
onChange={setRight}
56122
placeholder={t('diffRightPlaceholder')}
57-
aria-label={t('diffRightPlaceholder')}
58-
className="flex-1 min-h-[200px] p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-800 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-amber-500"
59-
spellCheck={false}
123+
ariaLabel={t('diffRightPlaceholder')}
124+
borderColor="amber"
60125
/>
61126
</div>
62127

@@ -75,7 +140,7 @@ export default function DiffView() {
75140
className={`px-3 py-1.5 transition-colors ${
76141
mode === 'keys'
77142
? 'bg-amber-500 text-white'
78-
: 'bg-white dark:bg-slate-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-700'
143+
: 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
79144
}`}
80145
>
81146
{t('diffByKeys')}
@@ -86,7 +151,7 @@ export default function DiffView() {
86151
className={`px-3 py-1.5 transition-colors ${
87152
mode === 'lines'
88153
? 'bg-amber-500 text-white'
89-
: 'bg-white dark:bg-slate-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-700'
154+
: 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
90155
}`}
91156
>
92157
{t('diffByLines')}
@@ -96,7 +161,7 @@ export default function DiffView() {
96161

97162
{hasResults && (
98163
<div className="rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden">
99-
<div className="bg-gray-100 dark:bg-slate-700 px-3 py-2 text-sm font-medium flex items-center justify-between">
164+
<div className="bg-gray-100 dark:bg-gray-700 px-3 py-2 text-sm font-medium flex items-center justify-between">
100165
<span>{t('diffResult')}</span>
101166
{mode === 'lines' && lineResult && !lineResult.error && (
102167
<span className="text-xs font-normal">

src/components/Editor.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useTranslation } from 'react-i18next';
2+
import { useFileDrop } from '../hooks/useFileDrop';
23

34
interface Props {
45
input: string;
@@ -11,21 +12,43 @@ interface Props {
1112

1213
export default function Editor({ input, output, error, copied, onInputChange, onCopy }: Props) {
1314
const { t } = useTranslation();
15+
const { isDragging, dropError, onDragOver, onDragLeave, onDrop } = useFileDrop(onInputChange);
1416

1517
return (
1618
<div className="flex flex-col lg:flex-row gap-4 h-full">
1719
<div className="flex-1 flex flex-col gap-2">
18-
<textarea
19-
value={input}
20-
onChange={(e) => onInputChange(e.target.value)}
21-
placeholder={t('inputPlaceholder')}
22-
aria-label={t('inputPlaceholder')}
23-
className="flex-1 min-h-[300px] w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-800 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-amber-500"
24-
spellCheck={false}
25-
/>
26-
{error && (
20+
<div
21+
className="relative flex-1"
22+
onDragOver={onDragOver}
23+
onDragLeave={onDragLeave}
24+
onDrop={onDrop}
25+
>
26+
<textarea
27+
value={input}
28+
onChange={(e) => onInputChange(e.target.value)}
29+
placeholder={t('inputPlaceholder')}
30+
aria-label={t('inputPlaceholder')}
31+
className={`flex-1 min-h-[300px] w-full h-full p-3 rounded-lg border bg-white dark:bg-gray-800 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-amber-500 ${
32+
isDragging
33+
? 'border-indigo-500 dark:border-indigo-400 border-2'
34+
: 'border-gray-300 dark:border-gray-600'
35+
}`}
36+
spellCheck={false}
37+
/>
38+
{isDragging && (
39+
<div
40+
className="absolute inset-0 flex items-center justify-center rounded-lg bg-indigo-500/10 dark:bg-indigo-400/10 pointer-events-none"
41+
aria-label={t('dropFileHere')}
42+
>
43+
<span className="text-indigo-600 dark:text-indigo-400 font-medium text-sm">
44+
{t('dropFileHere')}
45+
</span>
46+
</div>
47+
)}
48+
</div>
49+
{(error || dropError) && (
2750
<div className="text-red-600 dark:text-red-400 text-sm bg-red-50 dark:bg-red-900/20 p-2 rounded">
28-
{error}
51+
{error || dropError}
2952
</div>
3053
)}
3154
</div>
@@ -36,7 +59,7 @@ export default function Editor({ input, output, error, copied, onInputChange, on
3659
readOnly
3760
placeholder={t('outputPlaceholder')}
3861
aria-label={t('outputPlaceholder')}
39-
className="min-h-[300px] w-full h-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-slate-800/50 text-sm resize-none focus:outline-none"
62+
className="min-h-[300px] w-full h-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 text-sm resize-none focus:outline-none"
4063
spellCheck={false}
4164
/>
4265
{output && (

src/components/Footer.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
import { useTranslation } from 'react-i18next';
22

3+
const GITHUB_ICON = '<svg viewBox="0 0 16 16" class="inline w-4 h-4 fill-current align-text-bottom"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>';
4+
35
export default function Footer() {
46
const { t } = useTranslation();
57

68
return (
7-
<footer className="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-800 px-4 py-3">
8-
<div className="max-w-7xl mx-auto flex flex-wrap items-center justify-center gap-4 text-sm text-gray-500 dark:text-gray-400">
9+
<footer className="border-t border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900">
10+
<div className="mx-auto flex max-w-5xl items-center justify-center gap-2 px-4 py-4 text-xs text-gray-500 dark:text-gray-400">
911
<a
1012
href="https://github.com/alejandroSuch/JSONPretty"
1113
target="_blank"
1214
rel="noopener noreferrer"
13-
className="hover:text-amber-500 transition-colors"
15+
className="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
1416
>
15-
{t('viewOnGithub')}
17+
<span dangerouslySetInnerHTML={{ __html: GITHUB_ICON }} />
18+
<span>{t('viewOnGithub')}</span>
1619
</a>
17-
<span>·</span>
20+
<span>&middot;</span>
1821
<a
1922
href="https://buymeacoffee.com/alejandrosuch"
2023
target="_blank"
2124
rel="noopener noreferrer"
22-
className="hover:text-amber-500 transition-colors"
25+
className="inline-flex items-center gap-1 hover:text-yellow-600 dark:hover:text-yellow-400 transition-colors"
2326
>
24-
{t('buyMeACoffee')}
27+
<span></span>
28+
<span>{t('buyMeACoffee')}</span>
2529
</a>
2630
</div>
2731
</footer>

src/components/Header.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,39 @@ interface Props {
1818
export default function Header({ dark, toggleDark }: Props) {
1919
const { t, i18n } = useTranslation();
2020

21+
const theme = dark ? 'dark' : 'light';
22+
const icon = theme === 'dark' ? '🌙' : '☀️';
23+
2124
return (
22-
<header className="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-800 px-4 py-3">
23-
<div className="max-w-7xl mx-auto flex flex-wrap items-center justify-between gap-2">
25+
<header className="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900">
26+
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-4">
2427
<div className="flex items-center gap-3">
25-
<h1 className="text-xl font-bold text-amber-500">{t('appName')}</h1>
26-
<span className="hidden sm:inline text-sm text-gray-500 dark:text-gray-400">{t('tagline')}</span>
27-
<span className="hidden sm:inline text-xs bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-0.5 rounded-full">{t('privacyBadge')}</span>
28+
<img src={`${import.meta.env.BASE_URL}favicon.svg`} alt="JSONPretty" className="h-9 w-9" />
29+
<div>
30+
<h1 className="text-lg font-semibold text-amber-500 dark:text-amber-400">{t('appName')}</h1>
31+
<p className="hidden text-xs text-gray-500 dark:text-gray-400 sm:block">{t('tagline')}</p>
32+
</div>
33+
<span className="hidden rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/40 dark:text-green-400 sm:inline-flex">
34+
{t('privacyBadge')}
35+
</span>
2836
</div>
2937
<div className="flex items-center gap-2">
3038
<select
3139
value={i18n.language.substring(0, 2)}
3240
onChange={(e) => i18n.changeLanguage(e.target.value)}
33-
className="text-sm bg-gray-100 dark:bg-slate-700 border-none rounded px-2 py-1 cursor-pointer"
3441
aria-label={t('language')}
42+
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 focus:border-amber-500 focus:outline-none focus:ring-1 focus:ring-amber-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300"
3543
>
3644
{LANGUAGES.map((l) => (
3745
<option key={l.code} value={l.code}>{l.label}</option>
3846
))}
3947
</select>
4048
<button
4149
onClick={toggleDark}
42-
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
50+
className="rounded-md border border-gray-300 px-2 py-1 text-xs text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
4351
aria-label={t('toggleTheme')}
4452
>
45-
{dark ? '☀️' : '🌙'}
53+
{icon}
4654
</button>
4755
</div>
4856
</div>

src/components/Toolbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ export default function Toolbar({ mode, onFormat, onValidate, onToYaml, onToggle
1313

1414
const btn = 'px-3 py-1.5 text-sm font-medium rounded transition-colors';
1515
const primary = `${btn} bg-amber-500 hover:bg-amber-600 text-white`;
16-
const secondary = `${btn} bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-800 dark:text-gray-200`;
16+
const secondary = `${btn} bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200`;
1717
const active = `${btn} bg-amber-600 text-white ring-2 ring-amber-400`;
1818

1919
return (
20-
<div className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-800/50 px-4 py-2">
20+
<div className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 px-4 py-2">
2121
<div className="max-w-7xl mx-auto flex flex-wrap gap-2">
2222
<button onClick={onFormat} className={primary} disabled={mode === 'diff'} aria-label={t('format')}>{t('format')}</button>
2323
<button onClick={onValidate} className={secondary} disabled={mode === 'diff'} aria-label={t('validate')}>{t('validate')}</button>

0 commit comments

Comments
 (0)