mirror of
https://github.com/dyzulk/trustlab.git
synced 2026-01-27 05:51:54 +07:00
Refactor public legal URLs and fix broken links
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })}
|
||||
/>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
Reference in New Issue
Block a user