migrate all strings to strings.json, reorganize strings.json, bump packages, add todo, rm unnecessary import, minor error fixes

This commit is contained in:
Aidan 2025-01-30 19:59:24 -05:00
parent 05f9be8493
commit d52330d191
20 changed files with 265 additions and 163 deletions

View File

@ -85,3 +85,4 @@ A simple blogging platform built with Next.js, shadcn/ui and Tailwind CSS.
- [ ] Implement a logout animation on `/admin/logout`
- [ ] Add a post list w/ management options on `/admin/posts`
- [ ] Add a user list w/ management options in `/admin/users`
- [ ] Better error handling in `server/index.js`

View File

@ -2,6 +2,7 @@
export const dynamic = 'force-dynamic';
import { useState, useEffect } from 'react';
import strings from "@/strings.json"
export default function AdminLayout({ children }: { children: React.ReactNode }) {
const [loading, setLoading] = useState(true);
@ -9,7 +10,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
useEffect(() => {
if (window.location.pathname.startsWith('/admin/login')) {
console.log("[i] Detected login page, skipping validation");
console.log(strings.logsDetectedLoginPage);
setAuthorized(true);
setLoading(false);
return;
@ -24,7 +25,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
const validate = async () => {
const key = cookies.key;
if (!key) {
console.log("[!] No key found, clearing cookies and redirecting to login");
console.log(strings.errorsNoKeyFoundFancy);
document.cookie.split(';').forEach((cookie) => {
const [name] = cookie.split('=');
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
@ -41,16 +42,16 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
});
if (!response.ok) {
console.log('[!] Failed to check key, skipping validation and clearing cookie');
console.log(strings.errorsFailedKeyCheckFancy);
document.cookie = 'key=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
window.location.href = '/admin/login';
} else {
const data = await response.json()
if (data.success) {
console.log("[✓] Key is valid");
console.log(strings.logsKeyIsValid);
setAuthorized(true);
} else {
console.log("[✖] Key is invalid, clearing cookie and redirecting to login");
console.log(strings.errorsKeyInvalidFancy);
document.cookie = 'key=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
window.location.href = '/admin/login';
}

View File

@ -15,7 +15,7 @@ export default function Home() {
const [userCtCardLoading, setUserCtCardLoading] = useState(true);
useEffect(() => {
console.log("[i] Calculating post count...");
console.log(strings.logsCalculatingPostCt);
(async () => {
try {
const username = document.cookie.split('; ').find(row => row.startsWith('username='))?.split('=')[1] || '';
@ -34,9 +34,9 @@ export default function Home() {
});
if (!res.ok) {
alert('Failed to fetch total post count');
alert(strings.errorsFetchTotalPostCtErr);
setPostCardError(true);
throw new Error(`Failed to fetch total post count: ${res.status}`);
throw new Error(`${strings.errorsFetchTotalPostCtErr}: ${res.status}`);
}
const data = await res.json();
@ -47,25 +47,25 @@ export default function Home() {
setPostCardLoading(false);
throw new Error(data.message);
} else {
alert('Unknown error occurred');
alert(strings.errorsUnknownError);
setPostCardError(true);
setPostCardLoading(false);
throw new Error('Unknown error occurred');
throw new Error(strings.errorsUnknownError);
}
} else if (data.count) {
console.log("[✓] Total posts:", data.count);
console.log(strings.logsTotalPosts, data.count);
setTotalPosts(data.count);
setPostCardLoading(false);
}
} catch (error) {
alert('Error fetching total post count');
alert(strings.errorsFetchTotalPostCtErr);
setPostCardError(true);
setPostCardLoading(false);
console.error('[!] Failed to fetch total post count:', error);
console.error(strings.errorsFetchTotalPostCtErrFancy, error);
}
})();
console.log("[i] Calculating user count...");
console.log(strings.logsCalculatingUserCt);
(async () => {
try {
const username = document.cookie.split('; ').find(row => row.startsWith('username='))?.split('=')[1] || '';
@ -84,9 +84,9 @@ export default function Home() {
});
if (!res.ok) {
alert('Failed to fetch total user count');
alert(strings.errorsFetchTotalUserCtErr);
setUserCtCardError(true);
throw new Error(`Failed to fetch total user count: ${res.status}`);
throw new Error(`${strings.errorsFetchTotalUserCtErr}: ${res.status}`);
}
const data = await res.json();
@ -97,21 +97,21 @@ export default function Home() {
setUserCtCardLoading(false);
throw new Error(data.message);
} else {
alert('Unknown error occurred');
alert(strings.errorsUnknownError);
setUserCtCardError(true);
setUserCtCardLoading(false);
throw new Error('Unknown error occurred');
throw new Error(strings.errorsUnknownError);
}
} else if (data.count) {
console.log("[✓] Total users:", data.count);
console.log(strings.logsTotalUsers, data.count);
setTotalUsers(data.count);
setUserCtCardLoading(false);
}
} catch (error) {
alert('Error fetching total user count');
alert(strings.errorsFetchTotalUserCtErr);
setUserCtCardError(true);
setUserCtCardLoading(false);
console.error('[!] Failed to fetch total user count:', error);
console.error(strings.errorsFetchTotalUserCtErrFancy, error);
}
})();
}, []);
@ -130,7 +130,7 @@ export default function Home() {
) : userCtCardError ? (
<div className="flex items-center text-red-500">
<CircleAlert />
<p className="text-base ml-1.5">Error</p>
<p className="text-base ml-1.5">{strings.errorsSuperGeneric}</p>
</div>
) : (
totalUsers
@ -149,7 +149,7 @@ export default function Home() {
) : postCardError ? (
<div className="flex items-center text-red-500">
<CircleAlert />
<p className="text-base ml-1.5">Error</p>
<p className="text-base ml-1.5">{strings.errorsSuperGeneric}</p>
</div>
) : (
totalPosts
@ -162,19 +162,19 @@ export default function Home() {
<div className="grid gap-6 sm:grid-cols-3 lg:grid-cols-4">
<Card className="flex flex-col justify-between">
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardTitle>{strings.adminQuickActionsCardTitle}</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<Button className="w-full justify-start" variant="outline" asChild>
<a href="/admin/posts/new">
<PlusCircle className="mr-2 h-4 w-4" />
New Post
{strings.createPostButtonText}
</a>
</Button>
<Button className="w-full justify-start" variant="outline" asChild>
<a href="/admin/users/new">
<UserPlus className="mr-2 h-4 w-4" />
New User
{strings.createUserButtonText}
</a>
</Button>
</CardContent>

View File

@ -43,13 +43,13 @@ export default function CreatePost() {
});
if (!response.ok) {
throw new Error('Failed to create post');
throw new Error(strings.errorsCreatePostFail);
}
const data = await response.json();
console.log('Success:', data);
console.log(`${strings.successSuperGeneric}: ${data}`);
} catch (error) {
console.error('Error:', error);
console.error(`${strings.errorsSuperGeneric}: ${error}`);
}
console.log({ title, description, category, slug, content, date })
}

View File

@ -12,7 +12,7 @@ export default function Posts() {
const [postCardLoading, setPostCardLoading] = useState(true);
useEffect(() => {
console.log("[i] Calculating post count...");
console.log(strings.logsCalculatingPostCt);
(async () => {
try {
const username = document.cookie.split('; ').find(row => row.startsWith('username='))?.split('=')[1] || '';
@ -31,9 +31,9 @@ export default function Posts() {
});
if (!res.ok) {
alert('Failed to fetch total post count');
alert(strings.errorsFetchTotalPostCtErr);
setPostCardError(true);
throw new Error(`Failed to fetch total post count: ${res.status}`);
throw new Error(`${strings.errorsFetchTotalPostCtErr}: ${res.status}`);
}
const data = await res.json();
@ -44,38 +44,38 @@ export default function Posts() {
setPostCardLoading(false);
throw new Error(data.message);
} else {
alert('Unknown error occurred');
alert(strings.errorsUnknownError);
setPostCardError(true);
setPostCardLoading(false);
throw new Error('Unknown error occurred');
throw new Error(strings.errorsUnknownError);
}
} else if (data.count) {
console.log("[✓] Total posts:", data.count);
console.log(strings.logsTotalPosts, data.count);
setTotalPosts(data.count);
setPostCardLoading(false);
}
} catch (error) {
alert('Error fetching total post count');
alert(strings.errorsFetchTotalPostCtErr);
setPostCardError(true);
setPostCardLoading(false);
console.error('[!] Failed to fetch total post count:', error);
console.error(strings.errorsFetchTotalPostCtErrFancy, error);
}
})();
}, []);
return (
<div className="space-y-8">
<h1 className="text-4xl font-bold text-primary">Posts</h1>
<h1 className="text-4xl font-bold text-primary">{strings.postsHeader}</h1>
<div className="grid gap-6 sm:grid-cols-3 lg:grid-cols-4">
<Card className="flex flex-col justify-between">
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardTitle>{strings.adminQuickActionsCardTitle}</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<Button className="w-full justify-start" variant="outline" asChild>
<a href="/admin/post">
<PlusCircle className="mr-2 h-4 w-4" />
New Post
{strings.createPostButtonText}
</a>
</Button>
</CardContent>
@ -90,7 +90,7 @@ export default function Posts() {
) : postCardError ? (
<div className="flex items-center text-red-500">
<CircleAlert />
<p className="text-base ml-1.5">Error</p>
<p className="text-base ml-1.5">{strings.errorsSuperGeneric}</p>
</div>
) : (
totalPosts

View File

@ -24,13 +24,13 @@ export default function CreateUser() {
});
if (!response.ok) {
throw new Error('Failed to create user');
throw new Error(strings.errorsCreateUserFail);
}
const data = await response.json();
console.log('Success:', data);
console.log(`${strings.successSuperGeneric}: ${data}`);
} catch (error) {
console.error('Error:', error);
console.error(`${strings.errorsSuperGeneric}: ${error}`);
}
console.log({ username, email, password })
}

View File

@ -12,7 +12,7 @@ export default function Users() {
const [userCtCardLoading, setUserCtCardLoading] = useState(true);
useEffect(() => {
console.log("[i] Calculating user count...");
console.log(strings.logsCalculatingUserCt);
(async () => {
try {
const username = document.cookie.split('; ').find(row => row.startsWith('username='))?.split('=')[1] || '';
@ -31,9 +31,9 @@ export default function Users() {
});
if (!res.ok) {
alert('Failed to fetch total user count');
alert(strings.errorsFetchTotalUserCtErr);
setUserCtCardError(true);
throw new Error(`Failed to fetch total user count: ${res.status}`);
throw new Error(`${strings.errorsFetchTotalUserCtErr}: ${res.status}`);
}
const data = await res.json();
@ -44,21 +44,21 @@ export default function Users() {
setUserCtCardLoading(false);
throw new Error(data.message);
} else {
alert('Unknown error occurred');
alert(strings.errorsUnknownError);
setUserCtCardError(true);
setUserCtCardLoading(false);
throw new Error('Unknown error occurred');
throw new Error(strings.errorsUnknownError);
}
} else if (data.count) {
console.log("[✓] Total users:", data.count);
console.log(strings.logsTotalUsers, data.count);
setTotalUsers(data.count);
setUserCtCardLoading(false);
}
} catch (error) {
alert('Error fetching total user count');
alert(strings.errorsFetchTotalUserCtErr);
setUserCtCardError(true);
setUserCtCardLoading(false);
console.error('[!] Failed to fetch total user count:', error);
console.error(strings.errorsFetchTotalUserCtErrFancy, error);
}
})();
}, []);
@ -69,13 +69,13 @@ export default function Users() {
<div className="grid gap-6 sm:grid-cols-3 lg:grid-cols-4">
<Card className="flex flex-col justify-between">
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardTitle>{strings.adminQuickActionsCardTitle}</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<Button className="w-full justify-start" variant="outline" asChild>
<a href="/admin/users/new">
<UserPlus className="mr-2 h-4 w-4" />
New User
{strings.createUserButtonText}
</a>
</Button>
</CardContent>
@ -90,7 +90,7 @@ export default function Users() {
) : userCtCardError ? (
<div className="flex items-center text-red-500">
<CircleAlert />
<p className="text-base ml-1.5">Error</p>
<p className="text-base ml-1.5">{strings.errorsSuperGeneric}</p>
</div>
) : (
totalUsers

View File

@ -36,8 +36,8 @@ export default function Home() {
setLoading(false)
})
.catch((error) => {
console.error("[!] Error fetching data:", error)
setError("Failed to fetch data")
console.error(strings.errorsFetchDataErrCtxFancy, error)
setError(strings.errorsFetchDataFail)
setLoading(false)
})
}, [])

View File

@ -2,11 +2,18 @@
import { useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import Link from "next/link"
import strings from "@/strings.json"
import { formatDistanceToNow, format } from 'date-fns'
type Post = {
id: string;
title: string;
description: string;
date: number;
slug: string;
};
export default function CategorySlug() {
const [posts, setPosts] = useState([]);
const [error, setError] = useState('');
@ -14,7 +21,7 @@ export default function CategorySlug() {
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log("[i] Fetching post list...");
console.log(strings.logsFetchPostList);
(async () => {
try {
const catReq = await fetch(`http://localhost:3001/api/categories/fetchList`, {
@ -26,49 +33,53 @@ export default function CategorySlug() {
});
if (!catReq.ok) {
throw new Error(`Failed to fetch category list: ${catReq.status}`);
throw new Error(`${strings.errorsFetchCategoryListFailCtx} ${catReq.status}`);
}
if (!postReq.ok) {
throw new Error(`Failed to fetch post list: ${postReq.status}`);
throw new Error(`${strings.errorsFetchPostListFailCtx} ${postReq.status}`);
}
const catData = await catReq.json();
const postData = await postReq.json();
if (!catData) {
setError('Failed to fetch category list');
setError(strings.errorsFetchCategoryListFail);
} else {
console.log("[✓] Fetched categories");
console.log(strings.logsOnFetchedCategory);
const slug = window.location.pathname.split('/').slice(-1)[0];
const category = catData.categories.find((cat: { slug: string }) => cat.slug === slug);
if (category) {
console.log(`[✓] Found category: ${category.name}`);
console.log(`${strings.logsOnFoundCategory} ${category.name}`);
setCategory(category.name);
if (postData.success === false) {
if (postData.message) {
throw new Error(postData.message);
} else {
throw new Error('Unknown error occurred');
throw new Error(strings.errorsUnknownError);
}
} else {
const sortedPosts = postData.posts.sort((a, b) => b.date - a.date);
const sortedPosts = postData.posts.sort((a: Post, b: Post) => b.date - a.date);
setPosts(sortedPosts);
}
} else {
setError('Could not find requested category');
throw new Error(`Category with slug "${slug}" not found`);
setError(strings.errorsFetchCategoryNotFound);
throw new Error(strings.errorsFetchCategoryNotFound);
}
}
} catch (error) {
console.error('[!] Error fetching post list:', error);
setError('Failed to fetch post list. Please try again later.');
console.error(strings.errorsFetchPostListErrCtxFancy, error);
setError(strings.errorsFetchPostListErr);
} finally {
setLoading(false);
}
})();
}, []);
const formatDate = (timestamp) => {
interface FormatDate {
(timestamp: number): string;
}
const formatDate: FormatDate = (timestamp) => {
const date = new Date(timestamp * 1000);
const now = new Date();
if (date.getFullYear() !== now.getFullYear()) {

View File

@ -22,7 +22,7 @@ export default function Home() {
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log("[i] Fetching post list...");
console.log(strings.logsFetchPostList);
(async () => {
try {
const res = await fetch(`http://localhost:3001/api/posts/fetchList`, {
@ -30,7 +30,7 @@ export default function Home() {
});
if (!res.ok) {
throw new Error(`Failed to fetch post list: ${res.status}`);
throw new Error(`${strings.errorsFetchPostListFailCtx} ${res.status}`);
}
const data = await res.json();
@ -38,15 +38,15 @@ export default function Home() {
if (data.message) {
throw new Error(data.message);
} else {
throw new Error('Unknown error occurred');
throw new Error(strings.errorsUnknownError);
}
} else {
const sortedPosts: Post[] = data.posts.sort((a: Post, b: Post) => b.date - a.date);
setPosts(sortedPosts);
}
} catch (error) {
console.error('[!] Error fetching post list:', error);
setError('Failed to fetch post list. Please try again later.');
console.error(strings.errorsFetchPostListErrCtx, error);
setError(strings.errorsFetchPostListErr);
} finally {
setLoading(false);
}

View File

@ -5,6 +5,7 @@ import remarkGfm from 'remark-gfm';
import { useParams } from 'next/navigation';
import Image from 'next/image';
import { Tag } from 'lucide-react';
import strings from "@/strings.json"
export default function PostPage() {
const [markdown, setMarkdown] = useState('');
@ -24,18 +25,14 @@ export default function PostPage() {
}
function setPostData(postData: PostData) {
setPostTitle(postData.title || 'Untitled Post');
setPostTitle(postData.title || strings.blankPostTitlePlaceholder);
if (postData.date) {
console.log("[i] Date:", postData.date);
const date = new Date(Number(postData.date) * 1000)
console.log("[i] Date object:", date);
const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' };
console.log("[i] Date options:", options);
console.log("[i] Formatted date:", date.toLocaleDateString(undefined, options));
setPostDate(date.toLocaleDateString(undefined, options));
} else {
setPostDate('an unknown date');
setPostDate(strings.blankPostDatePlaceholder);
}
if (postData.category) {
@ -43,11 +40,11 @@ export default function PostPage() {
} else if (postData.message) {
setPostCategory(postData.message);
} else {
setPostCategory("Error");
setPostCategory(strings.errorsSuperGeneric);
}
}
console.log("[i] Navigated to slug: ", slug);
console.log(strings.logsOnSlugNavigate, slug);
(async () => {
try {
const res = await fetch(`http://localhost:3001/api/posts/get/${slug}`, {
@ -64,8 +61,8 @@ export default function PostPage() {
throw new Error(data.message);
} else {
setLoading(false);
setError('Unknown error occurred');
throw new Error('Unknown error occurred');
setError(strings.errorsUnknownError);
throw new Error(strings.errorsUnknownError);
}
}
} else {
@ -81,7 +78,7 @@ export default function PostPage() {
if (postData.message) {
setPostData(postData);
} else {
setPostCategory("Error");
setPostCategory(strings.errorsSuperGeneric);
}
}
@ -89,9 +86,9 @@ export default function PostPage() {
setLoading(false);
}
} catch (error) {
console.error('Error fetching post:', error);
setError('Could not load the post.');
setMarkdown('# Error\n\nCould not load the post.');
console.error(strings.errorsFetchPostErr, ':', error);
setError(strings.errorsFetchPostErr);
setMarkdown(strings.errorsFetchPostErrMd);
}
})();
}, [slug]);
@ -113,7 +110,7 @@ export default function PostPage() {
return (
<div>
<h1 className="text-4xl font-bold my-4">{postTitle}</h1>
<p className="italic text-muted-foreground">Published on {postDate}</p>
<p className="italic text-muted-foreground">{strings.postPublishedOnLabel} {postDate}</p>
<div className="flex flex-wrap gap-2 my-4">
<span className="border border-white text-bold px-3 py-1 rounded-md">
<Tag className="w-4 h-4 inline-block mr-1" /> {postCategory}

BIN
bun.lockb

Binary file not shown.

View File

@ -3,15 +3,10 @@
import { useState, useEffect } from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import strings from "@/strings.json"
export function LoginForm({
className,
@ -30,7 +25,7 @@ export function LoginForm({
}, {} as Record<string, string>);
if (cookies.key) {
console.log('[i] Key found in browser cookies, checking validity');
console.log(strings.logsFoundKey);
try {
const response = await fetch('http://localhost:3001/api/admin/validate', {
method: 'POST',
@ -41,17 +36,17 @@ export function LoginForm({
})
if (!response.ok) {
console.log('[!] Failed to check key, skipping validation and clearing cookie');
console.log(strings.errorsFailedKeyCheckFancy);
document.cookie = 'key=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
} else {
const data = await response.json()
if (data.valid) {
// key exists and is valid, user has no reason to use login
console.log("[✓] Key is valid, redirecting to admin panel");
console.log(strings.logsAdminLoginKeyIsValid);
window.location.href = '/admin';
} else {
// key exists, but the server reports its not the latest key
console.log("[!] Key is invalid, clearing cookie and redirecting to login");
console.log(strings.errorsFailedKeyCheckFancy);
document.cookie.split(";").forEach((cookie) => {
document.cookie = cookie
.replace(/^ +/, "")
@ -61,8 +56,8 @@ export function LoginForm({
}
}
} catch (error) {
console.error('[!]', error)
setError('Failed to connect to the server. Please try again later.')
console.error('[!]', error);
setError(strings.errorsConnectionFail);
}
}
};
@ -85,12 +80,12 @@ export function LoginForm({
})
if (!response.ok) {
console.log('[!] Failed to login');
setError('An unknown error occurred. Please try again later.')
console.log(strings.errorsLoginFailFancy);
setError(strings.errorsUnknownErrorLong)
} else {
const data = await response.json()
if (data.success && data.key) {
console.log("[✓] Login successful, redirecting to admin panel");
console.log(strings.logsAdminLoginSuccessful);
document.cookie = `key=${data.key}; path=/; secure; samesite=strict`;
document.cookie = `username=${username}; path=/; secure; samesite=strict`;
document.location.href = '/admin';
@ -98,13 +93,13 @@ export function LoginForm({
if (!data.success && data.message) {
setError(data.message)
} else {
setError('An unknown error occurred. Please try again later.')
setError(strings.errorsUnknownErrorLong)
}
}
}
} catch (error) {
console.error('[i]', error)
setError('Failed to connect to the server. Please try again later.')
setError(strings.errorsConnectionFail)
}
}
@ -112,9 +107,9 @@ export function LoginForm({
<div className={cn("relative flex min-h-screen flex-col", className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Administration Panel</CardTitle>
<CardTitle className="text-2xl">{strings.adminLoginCardTitle}</CardTitle>
<CardDescription>
Please authenticate with your credentials.
{strings.adminLoginCardDescription}
</CardDescription>
</CardHeader>
<CardContent>
@ -122,7 +117,7 @@ export function LoginForm({
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Label htmlFor="username">{strings.adminLoginUsernameFieldLabel}</Label>
<Input
id="username"
type="text"
@ -133,7 +128,7 @@ export function LoginForm({
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Label htmlFor="password">{strings.adminLoginPasswordFieldLabel}</Label>
</div>
<Input
id="password"
@ -144,7 +139,7 @@ export function LoginForm({
/>
</div>
<Button type="submit" className="w-full">
Login
{strings.adminLoginButtonText}
</Button>
</div>
</form>

View File

@ -48,11 +48,11 @@ export function Navbar() {
<div className="flex items-center space-x-2">
<Button variant="ghost" size="icon" className="md:hidden lg:hidden" onClick={toggleSidebar}>
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle sidebar</span>
<span className="sr-only">{strings.screenReaderToggleSidebar}</span>
</Button>
</div>
<div className="w-full flex-1 md:w-auto md:flex-none">
<Input placeholder="Search posts on desktop..." className="h-9 w-full md:hidden lg:hidden" />
<Input placeholder={strings.searchPlaceholder} className="h-9 w-full md:hidden lg:hidden" />
</div>
</div>
</div>

View File

@ -22,31 +22,31 @@ export function Sidebar() {
const [uniqueCategories, setUniqueCategories] = useState<{ name: string; slug: string }[]>([]);
useEffect(() => {
console.log("[i] Fetching post list...");
console.log(strings.logsFetchPostList);
fetch('http://localhost:3001/api/posts/fetchList')
.then(response => response.json())
.then(data => {
if (!data.posts) {
throw new Error('[!] Failed to fetch posts');
throw new Error(strings.errorsFetchPostsFailFancy);
}
console.log("[✓] Fetched posts");
console.log(strings.logsOnFetchedPosts);
setPosts(data.posts);
setLoadingPosts(false);
})
.catch(error => {
console.error(error);
setError(`[!] Error fetching posts: ${error.message}`);
setError(`${strings.errorsFetchPostsFailFancy}: ${error.message}`);
setLoadingPosts(false);
});
console.log("[i] Fetching category list...");
console.log(strings.logsFetchCategoryList);
fetch('http://localhost:3001/api/categories/fetchList')
.then(response => response.json())
.then(data => {
if (!data.categories) {
throw new Error('Failed to fetch categories');
throw new Error(strings.errorsFetchCategoryListFail);
}
console.log("[✓] Fetched categories");
console.log(strings.logsOnFetchedCategory);
const categories = data.categories.map((cat: { name: string, slug: string }) => ({
name: cat.name,
slug: cat.slug,
@ -56,7 +56,7 @@ export function Sidebar() {
})
.catch(error => {
console.error(error);
setError(`[!] Error fetching categories: ${error.message}`);
setError(`${strings.errorsFetchCategoriesErrCtxFancy} ${error.message}`);
setLoadingCategories(false);
});
}, []);
@ -64,12 +64,12 @@ export function Sidebar() {
return (
<aside className="fixed left-3 md:left-2 lg:left-5 top-15 z-30 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 overflow-y-auto bg-background px-4 py-6 md:sticky md:block md:w-[280px] lg:w-[300px]">
<div className="flex items-center justify-between mb-6">
<Link href="/" className="text-4xl font-bold text-primary">{process.env.NEXT_PUBLIC_BLOG_NAME || 'BlogPop'}</Link>
<Link href="/" className="text-4xl font-bold text-primary">{process.env.NEXT_PUBLIC_BLOG_NAME || strings.blogPop}</Link>
</div>
<div className="flex flex-1 items-center justify-start space-x-2 md:justify-center">
<div className="w-full flex-1 md:w-auto md:flex-none">
<Input
placeholder="Search posts..."
placeholder={strings.searchPlaceholder}
className="h-9 mb-8 w-full md:w-[250px] lg:w-[270px]"
/>
</div>
@ -86,7 +86,7 @@ export function Sidebar() {
) : error ? (
<div className="text-red-500">{error}</div>
) : posts.length === 0 ? (
<div>No recent posts available.</div>
<div>{strings.errorsNoRecentPosts}</div>
) : (
<ul className="space-y-2">
{posts
@ -97,7 +97,7 @@ export function Sidebar() {
<Link
href={`/posts/${post.slug}`}
className="text-sm text-muted-foreground hover:text-primary"
aria-label={`View post titled ${post.title}`}
aria-label={`${strings.viewSingular} ${post.title}`}
>
{post.title}
</Link>
@ -117,7 +117,7 @@ export function Sidebar() {
<div className="animate-spin rounded-full h-16 w-16 border-4 border-t-slate-800 border-white"></div>
</div>
) : uniqueCategories.length === 0 ? (
<div>No categories available.</div>
<div>{strings.errorsNoCategories}</div>
) : (
<ul className="space-y-2">
{uniqueCategories.map((category) => (
@ -125,7 +125,7 @@ export function Sidebar() {
<Link
href={`/category/${category.slug}`}
className="text-sm text-muted-foreground hover:text-primary"
aria-label={`View posts in category ${category.name}`}
aria-label={`${strings.categoriesViewCategoryAriaPrefix} ${category.name} ${strings.categorySingular}`}
>
{category.name}
</Link>

View File

@ -1,38 +1,38 @@
import Link from "next/link"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
//import strings from "@/strings.json"
import strings from "@/strings.json"
export function AdminSidebar() {
return (
<aside className="fixed left-3 md:left-2 lg:left-5 top-15 z-30 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 overflow-y-auto bg-background px-4 py-6 md:sticky md:block md:w-[280px] lg:w-[300px]">
<div className="flex items-center justify-between mb-8">
<Link href="/admin" className="text-4xl font-bold text-primary">BlogPop</Link>
<Link href="/admin" className="text-4xl font-bold text-primary">{strings.blogPop}</Link>
<span className="text-sm text-muted-foreground">v1.0.0</span>
</div>
<Card className="mb-6">
<CardHeader>
<CardTitle>Navigation</CardTitle>
<CardTitle>{strings.adminNavigationCardTitle}</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
<li>
<Link href="/admin" className="text-sm text-muted-foreground hover:text-primary">
Dashboard
{strings.adminSidebarDashboardLinkText}
</Link>
</li>
<li>
<Link href="/admin/posts" className="text-sm text-muted-foreground hover:text-primary">
Posts
{strings.adminSidebarPostsLinkText}
</Link>
</li>
<li>
<Link href="/admin/users" className="text-sm text-muted-foreground hover:text-primary">
Users
{strings.adminSidebarUsersLinkText}
</Link>
</li>
<li>
<Link href="/admin/logout" className="text-sm text-muted-foreground hover:text-primary">
Logout
{strings.adminSidebarLogoutLinkText}
</Link>
</li>
</ul>

View File

@ -4,6 +4,7 @@ import Link from "next/link"
import { Button } from "@/components/ui/button"
import { X } from "lucide-react"
import { useSidebar } from "@/context/SidebarContext"
import strings from "@/strings.json"
export function MobileAdminSidebar() {
const { isOpen, toggleSidebar } = useSidebar()
@ -16,25 +17,25 @@ export function MobileAdminSidebar() {
>
<div className="flex items-center justify-between mb-6 mt-12">
<Link href="/" className="text-4xl font-bold text-primary">
{process.env.BLOG_NAME || "BlogPop"}
{process.env.BLOG_NAME || strings.blogPop}
</Link>
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="md:hidden">
<X className="h-6 w-6" />
<span className="sr-only">Close sidebar</span>
<span className="sr-only">{strings.screenReaderCloseSidebar}</span>
</Button>
</div>
<nav className="flex flex-col space-y-4">
<Link href="/admin" className="text-lg text-muted-foreground hover:text-primary">
Dashboard
{strings.adminSidebarDashboardLinkText}
</Link>
<Link href="/admin/posts" className="text-lg text-muted-foreground hover:text-primary">
Posts
{strings.adminSidebarPostsLinkText}
</Link>
<Link href="/admin/users" className="text-lg text-muted-foreground hover:text-primary">
Users
{strings.adminSidebarUsersLinkText}
</Link>
<Link href="/admin/logout" className="text-lg text-muted-foreground hover:text-primary">
Logout
{strings.adminSidebarLogoutLinkText}
</Link>
</nav>
</aside>

View File

@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
import { X } from "lucide-react"
import { useSidebar } from "@/context/SidebarContext"
import config from "@/config.json"
import strings from "@/strings.json"
export function MobileSidebar() {
const { isOpen, toggleSidebar } = useSidebar()
@ -17,22 +18,19 @@ export function MobileSidebar() {
>
<div className="flex items-center justify-between mb-6 mt-12">
<Link href="/" className="text-4xl font-bold text-primary">
{process.env.BLOG_NAME || "BlogPop"}
{process.env.BLOG_NAME || strings.blogPop}
</Link>
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="md:hidden">
<X className="h-6 w-6" />
<span className="sr-only">Close sidebar</span>
<span className="sr-only">{strings.screenReaderCloseSidebar}</span>
</Button>
</div>
<nav className="flex flex-col space-y-4">
<Link href="/" className="text-lg text-muted-foreground hover:text-primary">
Home
{strings.homeLinkTextNavbar}
</Link>
<Link href="/categories" className="text-lg text-muted-foreground hover:text-primary">
Categories
</Link>
<Link href="/contact" className="text-lg text-muted-foreground hover:text-primary">
Contact
{strings.categoriesLinkTextNavbar}
</Link>
{config.personalWebsite && (
<Link href={config.personalWebsiteUrl} className="text-lg text-muted-foreground hover:text-primary">

View File

@ -9,10 +9,10 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-visually-hidden": "^1.1.1",
"@uiw/react-md-editor": "^4.0.5",
@ -21,7 +21,7 @@
"date-fns": "^4.1.0",
"geist": "^1.3.1",
"is-mobile": "^5.0.0",
"lucide-react": "^0.471.1",
"lucide-react": "^0.471.2",
"next": "15.1.4",
"next-themes": "^0.4.4",
"react": "^19.0.0",
@ -34,12 +34,12 @@
},
"devDependencies": {
"typescript": "^5.7.3",
"@types/node": "^20.17.13",
"@types/react": "^19.0.7",
"@types/node": "^20.17.16",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"eslint": "^9.18.0",
"eslint": "^9.19.0",
"eslint-config-next": "15.1.4",
"@eslint/eslintrc": "^3.2.0"
}

View File

@ -1,10 +1,18 @@
{
"blogPop": "BlogPop",
"latestPostsHeader": "Latest Posts",
"categoriesHeader": "Categories",
"adminHeader": "Admin",
"adminNewPostHeader": "New Post",
"adminNewUserHeader": "New User",
"usersHeader": "Users",
"postsHeader": "Posts",
"searchPlaceholder": "Search",
"screenReaderToggleSidebar": "Toggle sidebar",
"screenReaderCloseSidebar": "Close sidebar",
"homeLinkTextNavbar": "Home",
"categoriesLinkTextNavbar": "Categories",
@ -14,7 +22,10 @@
"newPostCardTitle": "Create New Post",
"newUserCardTitle": "Create User",
"totalUsersCardTitle": "Total Users",
"adminQuickActionsCardTitle": "Quick Actions",
"totalUsersLoggedInMonthCardTitle": "Users Logged In",
"adminNavigationCardTitle": "Navigation",
"adminLoginCardTitle": "Administration Panel",
"newPostTitleFieldLabel": "Title",
"newPostDescriptionFieldLabel": "Description",
@ -22,28 +33,34 @@
"newPostSlugFieldLabel": "Slug",
"newPostContentFieldLabel": "Post Content",
"newUserNameFieldLabel": "Name",
"newUserEmailFieldLabel": "Email",
"newUserPasswordFieldLabel": "Password",
"newPostTitleFieldPlaceholder": "Enter post title",
"newPostDescriptionFieldPlaceholder": "Enter post description",
"newPostCategoryFieldPlaceholder": "Select a category",
"newPostSlugFieldPlaceholder": "Enter post slug (i.e. /example-slug)",
"newUserNameFieldLabel": "Name",
"newUserEmailFieldLabel": "Email",
"newUserPasswordFieldLabel": "Password",
"newUserNameFieldPlaceholder": "Enter desired username",
"newUserEmailFieldPlaceholder": "Enter user email",
"newUserPasswordFieldPlaceholder": "Enter user password",
"categoriesCardDescriptionPre": "Explore posts from",
"adminLoginUsernameFieldLabel": "Username",
"adminLoginPasswordFieldLabel": "Password",
"totalUsersLoggedInMonthCardDescription": "This month",
"adminLoginCardDescription": "Please authenticate with your credentials.",
"categoriesCardDescriptionPre": "Explore posts from",
"categoriesLastUpdatedLabel": "Last updated",
"categoriesViewPostsFromLinkText": "View posts",
"recentPostsReadMoreFromLinkText": "Read more",
"categoriesViewCategoryAriaPrefix": "View the",
"categoriesPostUnitSingle": "post",
"categoriesPostUnitPlural": "posts",
@ -53,5 +70,86 @@
"createPostButtonText": "Create Post",
"createUserButtonText": "Create User",
"recentPostsPublishedOnLabel": "Published "
"adminLoginButtonText": "Login",
"viewSingular": "View",
"categorySingular": "Category",
"recentPostsPublishedOnLabel": "Published ",
"blankPostTitlePlaceholder": "Untitled Post",
"blankPostDatePlaceholder": "an unknown date",
"postPublishedOnLabel": "Published on",
"adminSidebarDashboardLinkText": "Dashboard",
"adminSidebarPostsLinkText": "Posts",
"adminSidebarUsersLinkText": "Users",
"adminSidebarLogoutLinkText": "Logout",
"logsDetectedLoginPage": "[i] Detected login page, skipping validation",
"logsFoundKey": "[i] Key found in browser cookies, checking validity...",
"logsCalculatingPostCt": "[i] Calculating post count...",
"logsCalculatingUserCt": "[i] Calculating user count...",
"logsFetchPostList": "[i] Fetching post list...",
"logsFetchCategoryList": "[i] Fetching category list...",
"logsOnSlugNavigate": "[i] Navigated to slug: ",
"logsOnFoundCategory": "[✓] Found category:",
"logsOnFetchedCategory": "[✓] Fetched categories",
"logsOnFetchedPosts": "[✓] Fetched posts",
"logsTotalPosts": "[✓] Total posts:",
"logsTotalUsers": "[✓] Total users:",
"logsKeyIsValid": "[✓] Key is valid",
"logsAdminLoginKeyIsValid": "[✓] Key is valid, redirecting to admin panel",
"logsAdminLoginSuccessful": "[✓] Login successful, redirecting to admin panel",
"successSuperGeneric": "Success",
"errorsSuperGeneric": "Error",
"errorsUnknownError": "An unknown error occurred.",
"errorsUnknownErrorLong": "An unknown error occurred. Please try again later.",
"errorsFetchPostListFailCtx": "Failed to fetch post list:",
"errorsFetchCategoryListFailCtx": "Failed to fetch category list:",
"errorsFetchTotalPostCtErrFancy": "[!] Failed to fetch total post count:",
"errorsFetchTotalUserCtErrFancy": "[!] Failed to fetch total user count:",
"errorsFetchPostsFailFancy": "[!] Failed to fetch posts",
"errorsLoginFailFancy": "[!] Failed to login",
"errorsFailedKeyCheckFancy": "[!] Failed to check key, skipping validation and clearing cookie",
"errorsFetchTotalPostCtErr": "Failed to fetch total post count",
"errorsFetchTotalUserCtErr": "Failed to fetch total user count",
"errorsConnectionFail": "Failed to connect to the server. Please try again later.",
"errorsFetchCategoryListFail": "Failed to fetch category list",
"errorsFetchDataFail": "Failed to fetch data",
"errorsCreateUserFail": "Failed to create user",
"errorsCreatePostFail": "Failed to create post",
"errorsFetchPostListErr": "Failed to fetch post list. Please try again later.",
"errorsFetchPostListErrCtx": "Error fetching post list:",
"errorsFetchPostListErrCtxFancy": "[!] Error fetching post list:",
"errorsFetchDataErrCtxFancy": "[!] Error fetching data:",
"errorsFetchCategoriesErrCtxFancy": "[!] Error fetching categories:",
"errorsFetchPostErr": "Error fetching post",
"errorsFetchPostErrMd": "# Error\n\nCould not load the post.",
"errorsNoKeyFoundFancy": "[!] No key found, clearing cookies and redirecting to login",
"errorsKeyInvalidFancy": "[✖] Key is invalid, clearing cookie and redirecting to login",
"errorsFetchCategoryNotFound": "Could not find requested category",
"errorsNoRecentPosts": "No recent posts available.",
"errorsNoCategories": "No categories available."
}