summaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-15 18:47:59 +0700
committerpolwex <polwex@sortug.com>2025-05-15 18:47:59 +0700
commit3d4b740e5a512db8fbdd934af2fbc9585fa00f0f (patch)
tree0f4fb0549c46f9cc341a72c76a4d834e417f4be4 /src/pages
parent05d13b6f166eae5c2de8fe6f6038819b1b6ba1a0 (diff)
thanks gemini, very pretty
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/test/client-modal.tsx59
-rw-r--r--src/pages/test/index.tsx34
-rw-r--r--src/pages/test/product-details-server.tsx340
-rw-r--r--src/pages/test/trigger-modal-button.tsx37
4 files changed, 470 insertions, 0 deletions
diff --git a/src/pages/test/client-modal.tsx b/src/pages/test/client-modal.tsx
new file mode 100644
index 0000000..293e28c
--- /dev/null
+++ b/src/pages/test/client-modal.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import { useState, ReactNode, useEffect } from "react";
+
+interface ClientModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ children: ReactNode; // This will receive the Server Component's output
+ title?: string;
+}
+
+export default function ClientModal({
+ isOpen,
+ onClose,
+ children,
+ title = "Modal",
+}: ClientModalProps) {
+ // Optional: Prevent body scroll when modal is open
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "unset";
+ }
+ return () => {
+ document.body.style.overflow = "unset";
+ };
+ }, [isOpen]);
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+ <div
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
+ onClick={onClose} // Close on overlay click
+ >
+ <div
+ className="p-6 bg-white rounded-lg shadow-xl w-11/12 max-w-lg"
+ onClick={(e) => e.stopPropagation()} // Prevent click from closing modal if clicking inside content
+ >
+ <div className="flex items-center justify-between mb-4">
+ <h2 className="text-xl font-semibold">{title}</h2>
+ <button
+ onClick={onClose}
+ className="text-gray-500 hover:text-gray-700"
+ aria-label="Close modal"
+ >
+ × {/* A simple 'X' close button */}
+ </button>
+ </div>
+ <div>
+ {children} {/* Server Component content will be rendered here */}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/src/pages/test/index.tsx b/src/pages/test/index.tsx
new file mode 100644
index 0000000..35ce5db
--- /dev/null
+++ b/src/pages/test/index.tsx
@@ -0,0 +1,34 @@
+// This is a Server Component by default
+import ProductDetailsServer from "./product-details-server";
+import TriggerModalButton from "./trigger-modal-button"; // We'll make this a client component to manage state
+
+export default function SomePage() {
+ const productIdForModal = "123"; // Or get this dynamically
+
+ return (
+ <main className="container p-8 mx-auto">
+ <h1 className="mb-6 text-3xl font-bold">
+ Modal with Server Component Content
+ </h1>
+ <p>
+ This page demonstrates opening a modal whose content is rendered by a
+ Server Component. The modal shell (open/close logic) is a Client
+ Component.
+ </p>
+
+ {/*
+ The TriggerModalButton will manage the modal's open/close state.
+ It will receive the Server Component as a child to pass to ClientModal.
+ */}
+ <TriggerModalButton
+ modalTitle={`Product Details for ID: ${productIdForModal}`}
+ >
+ <ProductDetailsServer word={"fantastic"} />
+ </TriggerModalButton>
+
+ <div className="mt-8">
+ <p>Other content on the page...</p>
+ </div>
+ </main>
+ );
+}
diff --git a/src/pages/test/product-details-server.tsx b/src/pages/test/product-details-server.tsx
new file mode 100644
index 0000000..552ff21
--- /dev/null
+++ b/src/pages/test/product-details-server.tsx
@@ -0,0 +1,340 @@
+// This is a Server Component
+import React from "react";
+import db from "@/lib/db";
+import {
+ Card,
+ CardHeader,
+ CardDescription,
+ CardContent,
+ CardFooter,
+ CardTitle,
+} from "@/components/ui/card";
+import { NLP } from "sortug-ai";
+import {
+ BookOpen,
+ Volume2,
+ Link as LinkIcon,
+ ChevronDown,
+ ChevronUp,
+ Search,
+ Info,
+ MessageSquareQuote,
+ Tags,
+ ListTree,
+ Lightbulb,
+} from "lucide-react";
+import {
+ Example,
+ SubSense,
+ RelatedEntry,
+ Sense,
+ WordData,
+} from "@/zoom/logic/types";
+
+export default async function Wordd({
+ word,
+ lang,
+}: {
+ word: string;
+ lang: string;
+}) {
+ const data = db.fetchWordBySpelling(word, "en");
+ console.log({ data });
+
+ if (!data) return <p>oh...</p>;
+ return (
+ <Card>
+ <CardHeader>
+ <CardTitle>
+ <h1 className="text-xl">{word}</h1>
+ </CardTitle>
+ <CardDescription>
+ <IpaDisplay ipaEntries={data.ipa} />
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ {/* Senses */}
+ <h2 className="text-2xl font-semibold text-gray-800 mb-4">
+ Meanings & Definitions
+ </h2>
+ {data.senses.map((sense, index) => (
+ <SenseCard key={index} senseData={sense} senseNumber={index + 1} />
+ ))}
+ </CardContent>
+ <CardFooter></CardFooter>
+ </Card>
+ );
+ // return (
+ // <div className="p-6">
+ // <h3 className="mb-2 text-2xl font-bold">{word}</h3>
+ // <p className="mb-1 text-xl text-green-600">${word.}</p>
+ // <p className="text-gray-700">{word}</p>
+ // <p className="mt-4 text-xs text-gray-500">
+ // Content rendered on the server at: {new Date().toLocaleTimeString()}
+ // </p>
+ // </div>
+ // );
+}
+
+// Helper component for IPA display
+const IpaDisplay = ({
+ ipaEntries,
+}: {
+ ipaEntries: Array<{ ipa: string; tags?: string[] }>;
+}) => {
+ if (!ipaEntries || ipaEntries.length === 0) return null;
+ return (
+ <div className="flex items-center space-x-2 flex-wrap">
+ {ipaEntries.map((entry, index) => {
+ const tags = entry.tags ? entry.tags : [];
+ return (
+ <span key={index} className="text-lg text-blue-600 font-serif">
+ {entry.ipa}{" "}
+ {tags.length > 0 && (
+ <span className="text-xs text-gray-500">({tags.join(", ")})</span>
+ )}
+ </span>
+ );
+ })}
+ <button
+ className="p-1 text-blue-500 hover:text-blue-700 transition-colors"
+ title="Pronounce"
+ // onClick={() => {
+ // /* Pronunciation logic would be client-side or a server roundtrip for audio file. */ alert(
+ // "Pronunciation feature not implemented for server component.",
+ // );
+ // }}
+ >
+ <Volume2 size={20} />
+ </button>
+ </div>
+ );
+};
+
+// Component for displaying examples
+const ExampleDisplay = ({ examples }: { examples: Example[] }) => {
+ if (!examples || examples.length === 0) return null;
+ return (
+ <div className="mt-2">
+ <h5 className="text-xs font-semibold text-gray-600 mb-1 flex items-center">
+ <MessageSquareQuote size={14} className="mr-1 text-gray-500" />
+ Examples:
+ </h5>
+ <ul className="list-disc list-inside pl-2 space-y-1">
+ {examples.map((ex, idx) => (
+ <li key={idx} className="text-xs text-gray-600">
+ <span className="italic">"{ex.text}"</span>
+ {ex.ref && (
+ <span className="text-gray-400 text-xxs"> ({ex.ref})</span>
+ )}
+ {ex.type !== "quote" && (
+ <span className="ml-1 text-xxs bg-sky-100 text-sky-700 px-1 rounded-sm">
+ {ex.type}
+ </span>
+ )}
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+};
+
+// Component for displaying related terms (synonyms, antonyms, etc.)
+const RelatedTermsDisplay = ({
+ terms,
+ type,
+}: {
+ terms: RelatedEntry[] | undefined;
+ type: string;
+}) => {
+ if (!terms || terms.length === 0) return null;
+ return (
+ <div className="mt-1">
+ <span className="text-xs font-semibold text-gray-500 capitalize">
+ {type}:{" "}
+ </span>
+ {terms.map((term, idx) => (
+ <React.Fragment key={idx}>
+ <a
+ href={`/search?q=${encodeURIComponent(term.word)}`}
+ className="text-xs text-blue-500 hover:text-blue-700 hover:underline"
+ >
+ {term.word}
+ </a>
+ {term.source && (
+ <span className="text-xxs text-gray-400"> ({term.source})</span>
+ )}
+ {idx < terms.length - 1 && ", "}
+ </React.Fragment>
+ ))}
+ </div>
+ );
+};
+
+// Component for displaying a SubSense
+const SubSenseDisplay = ({
+ subSense,
+ subSenseNumber,
+}: {
+ subSense: SubSense;
+ subSenseNumber: number;
+}) => {
+ return (
+ <div className="mb-3 pl-4 border-l-2 border-indigo-200">
+ {subSense.glosses.map((gloss, glossIdx) => (
+ <p key={glossIdx} className="text-gray-700 mb-1">
+ <span className="font-semibold">
+ {subSenseNumber}.{glossIdx + 1}
+ </span>{" "}
+ {gloss}
+ </p>
+ ))}
+ {subSense.raw_glosses &&
+ subSense.raw_glosses.length > 0 &&
+ subSense.raw_glosses.join("") !== subSense.glosses.join("") && (
+ <p className="text-xs text-gray-500 italic mb-1">
+ (Raw: {subSense.raw_glosses.join("; ")})
+ </p>
+ )}
+
+ {subSense.categories && subSense.categories.length > 0 && (
+ <div className="mt-1 mb-2">
+ <h5 className="text-xs font-semibold text-gray-600 mb-0.5 flex items-center">
+ <ListTree size={14} className="mr-1 text-gray-500" />
+ Categories:
+ </h5>
+ <div className="flex flex-wrap gap-1">
+ {subSense.categories.map((cat, idx) => (
+ <span
+ key={idx}
+ className="text-xxs bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded-full"
+ >
+ {cat}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ <ExampleDisplay examples={subSense.examples || []} />
+ <RelatedTermsDisplay terms={subSense.synonyms} type="Synonyms" />
+
+ {subSense.tags && subSense.tags.length > 0 && (
+ <div className="mt-2">
+ <h5 className="text-xs font-semibold text-gray-600 mb-0.5 flex items-center">
+ <Tags size={14} className="mr-1 text-gray-500" />
+ Tags:
+ </h5>
+ <div className="flex flex-wrap gap-1">
+ {subSense.tags.map((tag, idx) => (
+ <span
+ key={idx}
+ className="text-xxs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded-full"
+ >
+ {tag}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {subSense.links && subSense.links.length > 0 && (
+ <div className="mt-2">
+ {subSense.links.map(([type, target], linkIdx) => (
+ <a
+ key={linkIdx}
+ href={target} // Assuming target is a full URL or a path
+ target="_blank"
+ rel="noopener noreferrer"
+ className="text-xs text-blue-500 hover:text-blue-700 hover:underline mr-2 inline-flex items-center"
+ >
+ <LinkIcon size={12} className="mr-1" /> {type}
+ </a>
+ ))}
+ </div>
+ )}
+ </div>
+ );
+};
+
+// Component for individual sense
+const SenseCard = ({
+ senseData,
+ senseNumber,
+}: {
+ senseData: Sense;
+ senseNumber: number;
+}) => {
+ return (
+ <div className="mb-6 p-4 border border-gray-200 rounded-lg shadow-sm bg-white">
+ <div className="flex justify-between items-center mb-2">
+ <h3 className="text-xl font-semibold text-indigo-700">
+ {senseNumber}. {senseData.pos}
+ </h3>
+ </div>
+
+ {senseData.etymology && (
+ <details className="mb-3 group">
+ <summary className="cursor-pointer flex items-center text-sm text-gray-600 hover:text-indigo-600 transition-colors list-none">
+ Etymology
+ <ChevronDown size={16} className="ml-1 group-open:hidden" />
+ <ChevronUp size={16} className="ml-1 hidden group-open:inline" />
+ </summary>
+ <p className="mt-1 text-xs text-gray-500 italic bg-gray-50 p-2 rounded">
+ {senseData.etymology}
+ </p>
+ </details>
+ )}
+
+ {senseData.forms && senseData.forms.length > 0 && (
+ <div className="mb-3">
+ <h4 className="text-sm font-medium text-gray-700">Forms:</h4>
+ <div className="flex flex-wrap gap-2 mt-1">
+ {senseData.forms.map((form, idx) => (
+ <span
+ key={idx}
+ className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded-full"
+ >
+ {form.form}{" "}
+ {form.tags.length > 0 && `(${form.tags.join(", ")})`}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {senseData.senses.map((subSense, idx) => (
+ <SubSenseDisplay
+ key={idx}
+ subSense={subSense}
+ subSenseNumber={senseNumber}
+ />
+ ))}
+
+ {senseData.related && (
+ <div className="mt-3 pt-3 border-t border-gray-100">
+ <h4 className="text-sm font-medium text-gray-700 mb-1 flex items-center">
+ <Lightbulb size={16} className="mr-1 text-gray-500" />
+ Related Terms:
+ </h4>
+ <RelatedTermsDisplay
+ terms={senseData.related.related}
+ type="Related"
+ />
+ <RelatedTermsDisplay
+ terms={senseData.related.synonyms}
+ type="Synonyms (POS)"
+ />
+ <RelatedTermsDisplay
+ terms={senseData.related.antonyms}
+ type="Antonyms (POS)"
+ />
+ <RelatedTermsDisplay
+ terms={senseData.related.derived}
+ type="Derived"
+ />
+ </div>
+ )}
+ </div>
+ );
+};
diff --git a/src/pages/test/trigger-modal-button.tsx b/src/pages/test/trigger-modal-button.tsx
new file mode 100644
index 0000000..eaa2dda
--- /dev/null
+++ b/src/pages/test/trigger-modal-button.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { useState, ReactNode } from "react";
+import ClientModal from "./client-modal"; // The modal shell
+
+interface TriggerModalButtonProps {
+ children: ReactNode; // This will be the <ProductDetailsServer />
+ buttonText?: string;
+ modalTitle?: string;
+}
+
+export default function TriggerModalButton({
+ children,
+ buttonText = "Open Product Details",
+ modalTitle,
+}: TriggerModalButtonProps) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ return (
+ <>
+ <button
+ onClick={() => setIsModalOpen(true)}
+ className="px-4 py-2 font-semibold text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
+ >
+ {buttonText}
+ </button>
+
+ <ClientModal
+ isOpen={isModalOpen}
+ onClose={() => setIsModalOpen(false)}
+ title={modalTitle || ""}
+ >
+ {children} {/* Pass the Server Component content here */}
+ </ClientModal>
+ </>
+ );
+}