Refactor public legal URLs and fix broken links

This commit is contained in:
dyzulk
2025-12-30 18:02:36 +07:00
parent 59160de9ab
commit ca3641e338
9 changed files with 146 additions and 109 deletions

View File

@@ -1,22 +1,32 @@
"use client";
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, Suspense } from 'react';
import Link from 'next/link';
import { ArrowRight, Scale, FileText } from 'lucide-react';
import axios from '@/lib/axios';
import { useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
interface LegalPageItem {
title: string;
slug: string;
}
export default function LegalIndexPage() {
function LegalContent() {
const t = useTranslations("Legal");
const searchParams = useSearchParams();
const pageSlug = searchParams.get('page'); // Changed from 'slug' to 'page' as requested
const [pages, setPages] = useState<LegalPageItem[]>([]);
const [activePage, setActivePage] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
// Fetch List
useEffect(() => {
if (pageSlug) return; // Don't fetch list if viewing a page
const fetchPages = async () => {
try {
const { data } = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/api/public/legal-pages`);
@@ -28,8 +38,89 @@ export default function LegalIndexPage() {
}
};
fetchPages();
}, []);
}, [pageSlug]);
// Fetch Single Page
useEffect(() => {
if (!pageSlug) return;
const fetchPage = async () => {
setIsLoading(true);
try {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const res = await axios.get(`${apiUrl}/api/public/legal-pages/${pageSlug}`);
setActivePage(res.data.data);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
fetchPage();
}, [pageSlug]);
// View: Single Page
if (pageSlug) {
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="w-10 h-10 border-4 border-brand-500 border-t-transparent rounded-full animate-spin"></div>
</div>
);
}
if (!activePage) {
return (
<div className="min-h-[50vh] flex flex-col items-center justify-center">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">404 - Not Found</h2>
<Link href="/legal" className="text-brand-600 hover:underline">
{t('center_title')}
</Link>
</div>
);
}
return (
<div className="max-w-4xl mx-auto px-4 py-12">
<Link href="/legal" className="inline-flex items-center gap-2 text-sm text-gray-500 hover:text-brand-600 mb-8 transition-colors">
<ArrowRight className="rotate-180" size={16} />
{t('center_title')}
</Link>
<div className="bg-white dark:bg-gray-900 rounded-3xl shadow-xl overflow-hidden border border-gray-100 dark:border-gray-800">
<div className="p-8 md:p-12">
{/* Header */}
<div className="border-b border-gray-200 dark:border-gray-800 pb-8 mb-8">
<h1 className="text-3xl md:text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-brand-600 to-blue-600 dark:from-brand-400 dark:to-blue-400 mb-4">
{activePage.title}
</h1>
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
<span>{t('version')} {activePage.version}</span>
<span></span>
<span>{t('last_updated')}: {new Date(activePage.updated_at).toLocaleDateString()}</span>
</div>
</div>
{/* Styled Content */}
<article className="prose dark:prose-invert max-w-none
prose-headings:font-bold prose-headings:tracking-tight
prose-headings:!mt-6 prose-headings:!mb-4
prose-a:text-brand-600 dark:prose-a:text-brand-400 hover:prose-a:text-brand-500
prose-img:rounded-2xl prose-hr:border-gray-200 dark:prose-hr:border-gray-800
prose-p:leading-relaxed prose-p:mb-4
prose-ul:list-disc prose-ul:pl-6 prose-ul:mb-4
prose-ol:list-decimal prose-ol:pl-6 prose-ol:mb-4
prose-li:mb-1
whitespace-pre-wrap">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{activePage.content}</ReactMarkdown>
</article>
</div>
</div>
</div>
);
}
// View: Index List
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 pb-20">
{/* Hero Section */}
@@ -62,7 +153,7 @@ export default function LegalIndexPage() {
{pages.map((page) => (
<Link
key={page.slug}
href={`/legal/view?slug=${page.slug}`}
href={`/legal?page=${page.slug}`} // New URL structure
className="group bg-white dark:bg-gray-800 p-8 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300 flex items-center justify-between"
>
<div className="flex items-center gap-4">
@@ -96,3 +187,11 @@ export default function LegalIndexPage() {
</div>
);
}
export default function LegalIndexPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LegalContent />
</Suspense>
)
}

View File

@@ -1,94 +0,0 @@
"use client";
import React, { useEffect, useState, Suspense } from 'react';
import { notFound, useSearchParams } from 'next/navigation';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useTranslations } from 'next-intl';
function LegalPageContent() {
const t = useTranslations("Legal");
const searchParams = useSearchParams();
const slug = searchParams.get('slug');
const [page, setPage] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!slug) {
setLoading(false);
return;
}
const fetchPage = async () => {
try {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
const res = await fetch(`${apiUrl}/api/public/legal-pages/${slug}`, {
headers: { 'Accept': 'application/json' }
});
if (res.ok) {
const json = await res.json();
setPage(json.data);
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
fetchPage();
}, [slug]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="w-10 h-10 border-4 border-brand-500 border-t-transparent rounded-full animate-spin"></div>
</div>
);
}
if (!page) {
return notFound();
}
return (
<div className="max-w-4xl mx-auto px-4 py-12">
<div className="bg-white dark:bg-gray-900 rounded-3xl shadow-xl overflow-hidden border border-gray-100 dark:border-gray-800">
<div className="p-8 md:p-12">
{/* Header */}
<div className="border-b border-gray-200 dark:border-gray-800 pb-8 mb-8">
<h1 className="text-3xl md:text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-brand-600 to-blue-600 dark:from-brand-400 dark:to-blue-400 mb-4">
{page.title}
</h1>
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
<span>{t('version')} {page.version}</span>
<span></span>
<span>{t('last_updated')}: {new Date(page.updated_at).toLocaleDateString()}</span>
</div>
</div>
{/* Marketing/Styled Content */}
<article className="prose dark:prose-invert max-w-none
prose-headings:font-bold prose-headings:tracking-tight
prose-headings:!mt-1 prose-headings:!mb-0
prose-a:text-brand-600 dark:prose-a:text-brand-400 hover:prose-a:text-brand-500
prose-img:rounded-2xl prose-hr:border-gray-200 dark:prose-hr:border-gray-800
prose-p:!leading-tight prose-p:!my-0
prose-ul:!my-0 prose-ol:!my-0
prose-li:!my-0 prose-li:!leading-tight [&_li_p]:!my-0
whitespace-pre-wrap">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{page.content}</ReactMarkdown>
</article>
</div>
</div>
</div>
);
}
export default function PublicLegalPage() {
return (
<Suspense fallback={<div className="flex items-center justify-center min-h-[400px]"><div className="w-10 h-10 border-4 border-brand-500 border-t-transparent rounded-full animate-spin"></div></div>}>
<LegalPageContent />
</Suspense>
);
}

View File

@@ -241,7 +241,7 @@ export default function SigninClient() {
</Link>
</p>
<p className="mt-4 text-center text-xs text-gray-500 sm:text-start dark:text-gray-500">
{t('agree_to_text')} <Link href="/legal/view?slug=terms" className="underline hover:text-gray-700 dark:hover:text-gray-300">{t('terms_link')}</Link> {t('and_text')} <Link href="/legal/view?slug=privacy" className="underline hover:text-gray-700 dark:hover:text-gray-300">{t('privacy_policy_link')}</Link>.
{t('agree_to_text')} <Link href="/legal?page=terms" className="underline hover:text-gray-700 dark:hover:text-gray-300">{t('terms_link')}</Link> {t('and_text')} <Link href="/legal?page=privacy" className="underline hover:text-gray-700 dark:hover:text-gray-300">{t('privacy_policy_link')}</Link>.
</p>
</div>
</>

View File

@@ -188,7 +188,7 @@ export default function SignupClient() {
className="mr-3 h-5 w-5 rounded-md border-gray-300 dark:border-gray-700 text-brand-500 focus:ring-brand-500"
/>
<p className="inline-block font-normal text-gray-500 dark:text-gray-400">
{t('agree_to_signup_text')} <Link href="/legal/view?slug=terms" className="underline hover:text-gray-700 dark:hover:text-gray-300">{t('terms_link')}</Link> {t('and_text')} <Link href="/legal/view?slug=privacy" className="underline hover:text-gray-700 dark:hover:text-gray-300">{t('privacy_policy_link')}</Link>.
{t('agree_to_signup_text')} <Link href="/legal?page=terms" className="underline hover:text-gray-700 dark:hover:text-gray-300">{t('terms_link')}</Link> {t('and_text')} <Link href="/legal?page=privacy" className="underline hover:text-gray-700 dark:hover:text-gray-300">{t('privacy_policy_link')}</Link>.
</p>
</div>
</div>

View File

@@ -99,7 +99,21 @@ export default function AdminLegalEditorClient({ mode, initialData }: EditorProp
}
router.push("/dashboard/admin/legal");
} catch (error: any) {
addToast(error.response?.data?.message || t("toast_save_failed"), "error");
console.error(error);
const msg = error.response?.data?.message;
if (msg && msg.length < 150) {
addToast(msg, "error");
} else {
addToast(t("toast_error_generic"), "error");
}
// Handle Validation Errors
if (error.response?.status === 422 && error.response?.data?.errors) {
// You might want to set form errors state here if you had one
// For now just logging or maybe showing first error
const firstError = Object.values(error.response.data.errors)[0];
if (Array.isArray(firstError)) addToast(firstError[0] as string, "error");
}
} finally {
setIsSubmitting(false);
}
@@ -157,11 +171,11 @@ export default function AdminLegalEditorClient({ mode, initialData }: EditorProp
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
>
<option value="" disabled>{t("form_title_placeholder")}</option>
<option value="Privacy Policy">Privacy Policy</option>
<option value="Terms and Conditions">Terms and Conditions</option>
<option value="Cookie Policy">Cookie Policy</option>
<option value="Disclaimer">Disclaimer</option>
<option value="Acceptable Use Policy">Acceptable Use Policy</option>
<option value="Privacy Policy">{t("page_privacy_policy")}</option>
<option value="Terms and Conditions">{t("page_terms_conditions")}</option>
<option value="Cookie Policy">{t("page_cookie_policy")}</option>
<option value="Disclaimer">{t("page_disclaimer")}</option>
<option value="Acceptable Use Policy">{t("page_acceptable_use")}</option>
</select>
</div>
@@ -190,7 +204,7 @@ export default function AdminLegalEditorClient({ mode, initialData }: EditorProp
required
rows={20}
className="w-full px-4 py-4 bg-white dark:bg-gray-900 border-none focus:ring-0 font-mono text-sm text-gray-900 dark:text-gray-300 resize-y"
placeholder="# Privacy Policy\n\nWrite your content in Markdown..."
placeholder={t("placeholder_markdown_content")}
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
/>

View File

@@ -109,7 +109,7 @@ export default function AdminLegalListClient() {
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
<Link
href={`/legal/view?slug=${page.slug}`}
href={`/legal?page=${page.slug}`}
target="_blank"
className="p-1.5 text-gray-400 hover:text-brand-500 hover:bg-brand-50 dark:hover:bg-brand-900/20 rounded-lg transition-colors"
title={t("view_public")}