Switch navbar to #001321, admin can update logo/favicon, logo links home
This commit is contained in:
+120
-1
@@ -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',
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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' }}>
|
||||
<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',
|
||||
|
||||
Reference in New Issue
Block a user