这是indexloc提供的服务,不要输入任何密码
Skip to content

Create a details page for content to view the content read only #360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 0 additions & 47 deletions hosting/src/app/[site]/content/[type]/page.tsx

This file was deleted.

59 changes: 59 additions & 0 deletions hosting/src/app/[site]/document-type/[documentTypeId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
import DefaultLayout from "@/components/Layouts/DefaultLayout";
import {Table, TableRowActions, TableRowLabel} from "@/components/Table";
import Loader from "@/components/common/Loader";
import Notification from "@/components/common/Notification";
import {useTanamDocumentType} from "@/hooks/useTanamDocumentTypes";
import {useTanamDocuments} from "@/hooks/useTanamDocuments";
import {useTanamSite} from "@/hooks/useTanamSite";
import {useRouter} from "next/navigation";
import {Suspense} from "react";

export default function DocumentTypeDocumentsPage() {
const {data: documents, error: docsError} = useTanamDocuments();
const {data: tanamSite} = useTanamSite();
const {data: documentType} = useTanamDocumentType();
const router = useRouter();

const handleView = (documentId: string) => {
router.push(`/${tanamSite?.id}/document/${documentId}`);
};

return (
<DefaultLayout>
<Suspense fallback={<Loader />}>
{documentType ? <Breadcrumb pageName={documentType.titlePlural} /> : <Loader />}
</Suspense>

{docsError ? (
<Notification type="error" title="Error fetching documents" message={docsError.message} />
) : (
<Suspense fallback={<Loader />}>
<Table
headers={["Id", "Created", "Status", "Actions"]}
rows={documents.map((document, key) => [
<div key={`${key}-${document.id}-id`}>
<h5 className="font-medium text-black dark:text-white">{document.id}</h5>
</div>,
<p key={`${key}-${document.id}-date`} className="text-black dark:text-white">
{document.createdAt.toDate().toUTCString()}
</p>,
<TableRowLabel
key={`${key}-${document.id}-status`}
title={document.status}
status={document.status === "published" ? "success" : "info"}
/>,
<TableRowActions
key={`${key}-${document.id}-actions`}
onView={() => handleView(document.id)}
onDelete={() => console.log("Delete", document)}
onDownload={() => console.log("Download", document)}
/>,
])}
/>
</Suspense>
)}
</DefaultLayout>
);
}
96 changes: 96 additions & 0 deletions hosting/src/app/[site]/document/[documentId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use client";
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
import ContentCard from "@/components/Containers/ContentCard";
import {
Checkbox,
DatePicker,
Dropdown,
FileUpload,
FormGroup,
Input,
RadioButton,
Switcher,
TextArea,
} from "@/components/Form";
import DefaultLayout from "@/components/Layouts/DefaultLayout";
import Loader from "@/components/common/Loader";
import Notification from "@/components/common/Notification";
import {useTanamDocumentType} from "@/hooks/useTanamDocumentTypes";
import {useTanamDocument} from "@/hooks/useTanamDocuments";
import {TanamDocumentField} from "@/models/TanamDocumentField";
import {Timestamp} from "firebase/firestore";
import {Suspense} from "react";

const DocumentDetailsPage = () => {
const {data: document, error: docError} = useTanamDocument();
const {data: documentType, error: typeError} = useTanamDocumentType(document?.documentType);
const viewMode = true;

const renderFormElement = (field: TanamDocumentField, value: any) => {
switch (field.type) {
case "input-text":
return (
<FormGroup label={field.title} disabled={viewMode}>
<Input type="text" disabled={viewMode} placeholder={field.title} value={value || ""} />
</FormGroup>
);
case "textbox-rich":
return (
<FormGroup disabled={viewMode} label={field.title}>
<TextArea disabled={viewMode} rows={6} placeholder={field.title} value={value || ""} />
</FormGroup>
);
case "datepicker":
return (
<DatePicker
disabled={viewMode}
label={field.title}
placeholder="mm/dd/yyyy"
defaultValue={(value as Timestamp).toDate()}
/>
);
case "file-upload":
return <FileUpload disabled={viewMode} label={field.title} />;
case "switcher":
return <Switcher disabled={viewMode} defaultChecked={value} />;
case "radio":
return <RadioButton disabled={viewMode} label={field.title} />;
case "checkbox":
return <Checkbox />;
case "dropdown":
return <Dropdown disabled={viewMode} options={[]} placeholder={field.title} id={""} />;
default:
return null;
}
};

if (docError || typeError) {
return (
<DefaultLayout>
<Breadcrumb pageName={documentType?.titleSingular ?? "Document details"} />
<Notification
type="error"
title="Error loading document"
message={docError?.message || typeError?.message || "Unknown error"}
/>
</DefaultLayout>
);
}

return (
<DefaultLayout>
<Suspense fallback={<Loader />}>
{documentType ? <Breadcrumb pageName={documentType.titleSingular} /> : <Loader />}
</Suspense>
{documentType && document && (
<div className="grid grid-cols-1 gap-9">
<ContentCard key={document.id} title={document.data[documentType.documentTitleField]}>
{documentType.fields.map((field) => renderFormElement(field, document.data[field.key]))}
</ContentCard>
</div>
)}
</DefaultLayout>
);
};

export default DocumentDetailsPage;
13 changes: 11 additions & 2 deletions hosting/src/components/Form/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {BaseOptions} from "flatpickr/dist/types/options";
interface DatePickerProps {
label: string;
placeholder: string;
defaultValue?: Date;
onChange?: (date: Date) => void;
styleType?: "default" | "static" | "withArrows";
disabled?: boolean;
Expand All @@ -16,10 +17,18 @@ interface DatePickerProps {
* @param {DatePickerProps} props - The properties for the date picker component.
* @return {JSX.Element} The rendered date picker component.
*/
export function DatePicker({label, placeholder, onChange, styleType = "default", disabled = false}: DatePickerProps) {
export function DatePicker({
label,
placeholder,
defaultValue,
onChange,
styleType = "default",
disabled = false,
}: DatePickerProps) {
const config = {
mode: "single",
dateFormat: "M j, Y",
defaultDate: defaultValue,
onChange: (selectedDates: Date[]) => {
if (onChange && selectedDates.length > 0) {
onChange(selectedDates[0]);
Expand Down Expand Up @@ -53,7 +62,7 @@ export function DatePicker({label, placeholder, onChange, styleType = "default",
fp.destroy();
}
};
}, [onChange, styleType]);
}, [onChange, styleType, defaultValue]);

return (
<div className={`mb-4.5 ${disabled && "cursor-not-allowed opacity-50"}`}>
Expand Down
4 changes: 2 additions & 2 deletions hosting/src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ const Sidebar = ({sidebarOpen, setSidebarOpen}: SidebarProps) => {
title="Content"
isExpanded={pathname.includes("/content/")}
menuItems={documentTypes.map((doc) => ({
href: `/${site?.id}/content/${doc.id}`,
title: doc.title,
href: `/${site?.id}/document-type/${doc.id}`,
title: doc.titleSingular,
}))}
/>
<SidebarMenuItem href="/settings" icon={<SettingsIcon />} title="Settings" />
Expand Down
7 changes: 5 additions & 2 deletions hosting/src/hooks/useTanamDocumentTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export function useTanamDocumentTypes(): TanamDocumentTypeHook {
* @return {SingleTanamDocumentTypeHook} Hook for single document type subscription
*/
export function useTanamDocumentType(documentTypeId?: string): SingleTanamDocumentTypeHook {
const {site, type: paramType} = useParams<{site: string; type: string}>() ?? {site: null, type: null};
const {site, documentTypeId: paramType} = useParams<{site: string; documentTypeId: string}>() ?? {
site: null,
documentTypeId: null,
};
const typeId = documentTypeId ?? paramType;
const [data, setData] = useState<TanamDocumentType | null>(null);
const [error, setError] = useState<Error | null>(null);
Expand All @@ -73,7 +76,7 @@ export function useTanamDocumentType(documentTypeId?: string): SingleTanamDocume
return;
}
if (!typeId) {
setError(new Error("Document type ID parameter is missing"));
setData(null);
return;
}

Expand Down
62 changes: 57 additions & 5 deletions hosting/src/hooks/useTanamDocuments.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {firestore} from "@/firebase";
import {TanamDocument} from "@/models/TanamDocument";
import {collection, onSnapshot, query, where} from "firebase/firestore";
import {collection, doc, onSnapshot, query, where} from "firebase/firestore";
import {useParams} from "next/navigation";
import {useEffect, useState} from "react";

Expand All @@ -12,12 +12,15 @@ interface UseTanamDocumentsResult {
/**
* Hook to get a stream of documents of a specific content type
*
* @param {string?} documentType Optional document type (default to content parameter from URL).
* @param {string?} documentTypeId Optional document type (default to content parameter from URL).
* @return {UseTanamDocumentsResult} Hook for documents subscription
*/
export function useTanamDocuments(documentType?: string): UseTanamDocumentsResult {
const {site, type: paramType} = useParams<{site: string; type: string}>() ?? {site: null, content: null};
const type = documentType ?? paramType;
export function useTanamDocuments(documentTypeId?: string): UseTanamDocumentsResult {
const {site, documentTypeId: paramType} = useParams<{site: string; documentTypeId: string}>() ?? {
site: null,
documentTypeId: null,
};
const type = documentTypeId ?? paramType;
const [data, setData] = useState<TanamDocument[]>([]);
const [error, setError] = useState<Error | null>(null);

Expand Down Expand Up @@ -56,3 +59,52 @@ export function useTanamDocuments(documentType?: string): UseTanamDocumentsResul

return {data, error};
}

interface UseTanamDocumentResult {
data: TanamDocument | null;
error: Error | null;
}

/**
* Hook to get a subscription for a single document
*
* @param {string?} documentId Optional document id (default to content parameter from URL).
* @return {UseTanamDocumentsResult} Hook for document subscription
*/
export function useTanamDocument(documentId?: string): UseTanamDocumentResult {
const {site, documentId: paramId} = useParams<{site: string; documentId: string}>() ?? {site: null, documentId: null};
const id = documentId ?? paramId;
const [data, setData] = useState<TanamDocument | null>(null);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
if (!site) {
setError(new Error("Site parameter is missing"));
return;
}
if (!id) {
setError(new Error("Document id parameter is missing"));
return;
}
const docRef = doc(firestore, "tanam", site, "documents", id);
const unsubscribe = onSnapshot(
docRef,
(snapshot) => {
setData(
TanamDocument.fromJson({
id: snapshot.id,
...snapshot.data(),
}),
);
},
(err) => {
setError(err);
},
);

// Cleanup subscription on unmount
return () => unsubscribe();
}, [site, paramId]);

return {data, error};
}
43 changes: 43 additions & 0 deletions hosting/src/models/TanamDocumentField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Tanam document field model.
*/
export class TanamDocumentField {
/**
* Constructor.
*
* @param {string} key The key of the field
* @param {string} title The title of the field
* @param {string} type The type of the field
* @param {string[] | null} validators The validators for the field
*/
constructor(
public readonly key: string,
public readonly title: string,
public readonly type: string,
public readonly validators: string[] | null,
) {}

/**
* Static factory constructor.
*
* @param {any} json JSON representation of the field
* @return {TanamDocumentField} Document field instance
*/
static fromJson(json: any): TanamDocumentField {
return new TanamDocumentField(json.key, json.title, json.type, json.validators);
}

/**
* Serialize to JSON for Firestore.
*
* @return {any} JSON representation of the document field
*/
toJson(): any {
return {
key: this.key,
title: this.title,
type: this.type,
validators: this.validators,
};
}
}
Loading