Switch navbar to #001321, admin can update logo/favicon, logo links home

This commit is contained in:
2026-06-27 16:44:17 -05:00
parent 8952bbf555
commit 1148f7a72f
3 changed files with 192 additions and 7 deletions
+120 -1
View File
@@ -11,7 +11,7 @@ interface ImageRecord {
}
const gold = '#E8A849';
const navy = '#010D1E';
const navy = '#001321';
const warm = '#a6683c';
export default function AdminPage() {
@@ -20,6 +20,9 @@ export default function AdminPage() {
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [logoUrl, setLogoUrl] = useState('');
const [faviconUrl, setFaviconUrl] = useState('');
const [message, setMessage] = useState<string | null>(null);
useEffect(() => {
fetch('/api/admin/images', { credentials: 'same-origin' })
@@ -37,8 +40,50 @@ export default function AdminPage() {
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
fetch('/api/site-assets?key=logo')
.then(async (res) => {
if (!res.ok) return;
const data = await res.json();
if (data.data) setLogoUrl(data.data);
})
.catch(() => {});
fetch('/api/site-assets?key=favicon')
.then(async (res) => {
if (!res.ok) return;
const data = await res.json();
if (data.data) setFaviconUrl(data.data);
})
.catch(() => {});
}, [router]);
useEffect(() => {
if (!faviconUrl) return;
let link = document.querySelector("link[rel~='icon']") as HTMLLinkElement | null;
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = faviconUrl;
}, [faviconUrl]);
const updateAsset = async (key: string, value: string) => {
const res = await fetch('/api/site-assets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, value }),
credentials: 'same-origin',
});
if (!res.ok) {
setError('Failed to save ' + key);
return;
}
setMessage(`${key} updated. Refresh the page to see the change.`);
setTimeout(() => setMessage(null), 4000);
};
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
@@ -94,6 +139,80 @@ export default function AdminPage() {
Total uploaded images: <strong style={{ color: warm }}>{count}</strong>
</p>
{error && <p style={{ color: '#c23b22', marginBottom: '1rem' }}>{error}</p>}
{message && <p style={{ color: '#3b7a22', marginBottom: '1rem' }}>{message}</p>}
<section
style={{
marginBottom: '2rem',
padding: '1.5rem',
background: '#f5f0e8',
borderRadius: '16px',
display: 'flex',
flexDirection: 'column',
gap: '1rem',
}}
>
<h2 style={{ fontSize: '18px', color: navy, margin: 0 }}>Brand Assets</h2>
<div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))' }}>
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '13px', color: '#555' }}>
Navbar logo
<input
type="text"
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
placeholder="Paste image data URL or upload a small image below"
style={{ padding: '0.6rem 0.8rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.18)' }}
/>
</label>
<button
onClick={() => updateAsset('logo', logoUrl)}
disabled={!logoUrl}
style={{
padding: '0.7rem 1.2rem',
background: gold,
color: navy,
border: 'none',
borderRadius: '999px',
cursor: logoUrl ? 'pointer' : 'not-allowed',
fontWeight: 600,
alignSelf: 'end',
}}
>
Save logo
</button>
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', fontSize: '13px', color: '#555' }}>
Favicon
<input
type="text"
value={faviconUrl}
onChange={(e) => setFaviconUrl(e.target.value)}
placeholder="Paste image data URL or upload a small image below"
style={{ padding: '0.6rem 0.8rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.18)' }}
/>
</label>
<button
onClick={() => updateAsset('favicon', faviconUrl)}
disabled={!faviconUrl}
style={{
padding: '0.7rem 1.2rem',
background: gold,
color: navy,
border: 'none',
borderRadius: '999px',
cursor: faviconUrl ? 'pointer' : 'not-allowed',
fontWeight: 600,
alignSelf: 'end',
}}
>
Save favicon
</button>
</div>
<p style={{ margin: 0, fontSize: '13px', color: '#777' }}>
Tip: click an uploaded gallery image to preview, copy its data URL, and paste it above.
</p>
</section>
<label
style={{
display: 'inline-flex',
+35
View File
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/auth';
import { db } from '@/lib/db';
export async function GET(request: NextRequest) {
const key = request.nextUrl.searchParams.get('key');
if (!key) {
return NextResponse.json({ error: 'Missing key' }, { status: 400 });
}
const { rows } = await db.query('SELECT value FROM config WHERE key = $1', [key]);
return NextResponse.json({ data: rows[0]?.value || null });
}
export async function POST(request: NextRequest) {
try {
await requireRole('admin');
const body = await request.json();
const { key, value } = body;
if (!key || typeof value !== 'string') {
return NextResponse.json({ error: 'Missing key or value' }, { status: 400 });
}
await db.query(
`INSERT INTO config (key, value) VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`,
[key, value]
);
return NextResponse.json({ ok: true });
} catch (err: any) {
return NextResponse.json({ error: err.message || 'Failed to save asset' }, { status: 401 });
}
}
+37 -6
View File
@@ -15,6 +15,7 @@ const links = [
export default function Navbar() {
const [auth, setAuth] = useState<{ authenticated: boolean; role?: string } | null>(null);
const [logo, setLogo] = useState<string | null>(null);
const router = useRouter();
const pathname = usePathname();
@@ -23,6 +24,30 @@ export default function Navbar() {
.then((res) => (res.ok ? res.json() : { authenticated: false }))
.then((data) => setAuth(data))
.catch(() => setAuth({ authenticated: false }));
fetch('/api/site-assets?key=logo')
.then(async (res) => {
if (!res.ok) return null;
const data = await res.json();
return data.data || null;
})
.then((url) => setLogo(url))
.catch(() => setLogo(null));
fetch('/api/site-assets?key=favicon')
.then(async (res) => {
if (!res.ok) return;
const data = await res.json();
if (!data.data) return;
let link = document.querySelector("link[rel~='icon']") as HTMLLinkElement | null;
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = data.data;
})
.catch(() => {});
}, []);
const handleLogout = async () => {
@@ -37,10 +62,10 @@ export default function Navbar() {
display: 'flex',
gap: '1.5rem',
alignItems: 'center',
padding: '0.75rem 1.5rem',
background: '#010D1E',
padding: '0.6rem 1.5rem',
background: '#001321',
color: '#f5f0e8',
boxShadow: '0 2px 8px rgba(1,13,30,0.25)',
boxShadow: '0 2px 8px rgba(0,19,33,0.35)',
zIndex: 50,
};
@@ -60,8 +85,14 @@ export default function Navbar() {
return (
<nav style={navStyle}>
<Link href="/" style={{ fontWeight: 700, color: '#E8A849', textDecoration: 'none', fontSize: '20px', fontStyle: 'italic' }}>
La Huasca
<Link href="/" style={{ display: 'flex', alignItems: 'center', gap: '12px', textDecoration: 'none' }}>
{logo ? (
<img src={logo} alt="La Huasca" style={{ height: '36px', width: 'auto', maxWidth: '160px', objectFit: 'contain' }} />
) : (
<span style={{ fontWeight: 700, color: '#E8A849', fontSize: '20px', fontStyle: 'italic' }}>
La Huasca
</span>
)}
</Link>
{links.map((link) => (
<Link
@@ -97,7 +128,7 @@ export default function Navbar() {
href="/login"
style={{
marginLeft: 'auto',
color: '#010D1E',
color: '#001321',
textDecoration: 'none',
background: '#E8A849',
padding: '0.45rem 0.9rem',