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 gold = '#E8A849';
|
||||||
const navy = '#010D1E';
|
const navy = '#001321';
|
||||||
const warm = '#a6683c';
|
const warm = '#a6683c';
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
@@ -20,6 +20,9 @@ export default function AdminPage() {
|
|||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [logoUrl, setLogoUrl] = useState('');
|
||||||
|
const [faviconUrl, setFaviconUrl] = useState('');
|
||||||
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/admin/images', { credentials: 'same-origin' })
|
fetch('/api/admin/images', { credentials: 'same-origin' })
|
||||||
@@ -37,8 +40,50 @@ export default function AdminPage() {
|
|||||||
})
|
})
|
||||||
.catch((err) => setError(err.message))
|
.catch((err) => setError(err.message))
|
||||||
.finally(() => setLoading(false));
|
.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]);
|
}, [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 handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
@@ -94,6 +139,80 @@ export default function AdminPage() {
|
|||||||
Total uploaded images: <strong style={{ color: warm }}>{count}</strong>
|
Total uploaded images: <strong style={{ color: warm }}>{count}</strong>
|
||||||
</p>
|
</p>
|
||||||
{error && <p style={{ color: '#c23b22', marginBottom: '1rem' }}>{error}</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
|
<label
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-flex',
|
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() {
|
export default function Navbar() {
|
||||||
const [auth, setAuth] = useState<{ authenticated: boolean; role?: string } | null>(null);
|
const [auth, setAuth] = useState<{ authenticated: boolean; role?: string } | null>(null);
|
||||||
|
const [logo, setLogo] = useState<string | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
@@ -23,6 +24,30 @@ export default function Navbar() {
|
|||||||
.then((res) => (res.ok ? res.json() : { authenticated: false }))
|
.then((res) => (res.ok ? res.json() : { authenticated: false }))
|
||||||
.then((data) => setAuth(data))
|
.then((data) => setAuth(data))
|
||||||
.catch(() => setAuth({ authenticated: false }));
|
.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 () => {
|
const handleLogout = async () => {
|
||||||
@@ -37,10 +62,10 @@ export default function Navbar() {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '1.5rem',
|
gap: '1.5rem',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '0.75rem 1.5rem',
|
padding: '0.6rem 1.5rem',
|
||||||
background: '#010D1E',
|
background: '#001321',
|
||||||
color: '#f5f0e8',
|
color: '#f5f0e8',
|
||||||
boxShadow: '0 2px 8px rgba(1,13,30,0.25)',
|
boxShadow: '0 2px 8px rgba(0,19,33,0.35)',
|
||||||
zIndex: 50,
|
zIndex: 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,8 +85,14 @@ export default function Navbar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav style={navStyle}>
|
<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' }}>
|
||||||
La Huasca
|
{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>
|
</Link>
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -97,7 +128,7 @@ export default function Navbar() {
|
|||||||
href="/login"
|
href="/login"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
color: '#010D1E',
|
color: '#001321',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
background: '#E8A849',
|
background: '#E8A849',
|
||||||
padding: '0.45rem 0.9rem',
|
padding: '0.45rem 0.9rem',
|
||||||
|
|||||||
Reference in New Issue
Block a user