diff --git a/apps/embed-v1/next.config.js b/apps/embed-v1/next.config.js index 861c526..02049ca 100644 --- a/apps/embed-v1/next.config.js +++ b/apps/embed-v1/next.config.js @@ -1,8 +1,8 @@ import { fileURLToPath } from "url"; -import createJiti from "jiti"; +import { createJiti } from "jiti"; // Import env files to validate at build time. Use jiti so we can load .ts files in here. -createJiti(fileURLToPath(import.meta.url))("./src/env"); +await createJiti(fileURLToPath(import.meta.url)).import("./src/env"); /** @type {import("next").NextConfig} */ const config = { @@ -20,6 +20,17 @@ const config = { /** We already do linting and typechecking as separate tasks in CI */ eslint: { ignoreDuringBuilds: true }, typescript: { ignoreBuildErrors: true }, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "loremflickr.com", + port: "", + pathname: "/**", + }, + ], + unoptimized: true, + }, }; export default config; diff --git a/apps/embed-v1/src/actions/getOrgTestimonials.tsx b/apps/embed-v1/src/actions/getOrgTestimonials.tsx new file mode 100644 index 0000000..6bd22e3 --- /dev/null +++ b/apps/embed-v1/src/actions/getOrgTestimonials.tsx @@ -0,0 +1,31 @@ +"use server"; + +import { eq } from "@acme/db"; +import { db } from "@acme/db/client"; + +import type { + OrganizationTestimonialType, + TestimonialTableType, +} from "~/types/schema-types"; + +export async function getOrgTestimonials(org: string) { + try { + const data = (await db.query.organizationTable.findMany({ + where: (organizationTable, { eq }) => + eq(organizationTable.organizationName, org), + with: { + testimonials: { + where: (testimonialsTable: TestimonialTableType) => + eq(testimonialsTable.wallOfFame, true), + }, + }, + })) as OrganizationTestimonialType[]; + + if (data.length === 0) { + return "Project not found"; + } + return data[0]; + } catch (error) { + return (error as Error).message; + } +} diff --git a/apps/embed-v1/src/app/[orgName]/page.tsx b/apps/embed-v1/src/app/[orgName]/page.tsx deleted file mode 100644 index f4ce82e..0000000 --- a/apps/embed-v1/src/app/[orgName]/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from "react"; - -export default function page({ params }: { params: { orgName: string } }) { - const { orgName } = params; - return
{orgName}
; -} diff --git a/apps/embed-v1/src/app/globals.css b/apps/embed-v1/src/app/globals.css index b9d992f..6137683 100644 --- a/apps/embed-v1/src/app/globals.css +++ b/apps/embed-v1/src/app/globals.css @@ -5,46 +5,62 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 240 10% 3.9%; + --foreground: 250 10% 3.9%; --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; + --card-foreground: 250 10% 3.9%; --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 327 66% 69%; - --primary-foreground: 337 65.5% 17.1%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; + --popover-foreground: 250 10% 3.9%; + --primary: 267 75% 64%; + --primary-foreground: 0 0% 100%; + --secondary: 250 4.8% 95.9%; + --secondary-foreground: 250 5.9% 10%; + --muted: 250 4.8% 95.9%; + --muted-foreground: 250 3.8% 46.1%; + --accent: 267 75% 95.9%; + --accent-foreground: 250 5.9% 10%; --destructive: 0 72.22% 50.59%; --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 5% 64.9%; + --border: 250 5.9% 90%; + --input: 250 5.9% 90%; + --ring: 267 75% 64%; --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { - --background: 240 10% 3.9%; + --background: 250 10% 3.9%; --foreground: 0 0% 98%; - --card: 240 10% 3.9%; + --card: 250 10% 3.9%; --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; + --popover: 250 10% 3.9%; --popover-foreground: 0 0% 98%; - --primary: 327 66% 69%; - --primary-foreground: 337 65.5% 17.1%; - --secondary: 240 3.7% 15.9%; + --primary: 267 75% 64%; + --primary-foreground: 0 0% 100%; + --secondary: 250 3.7% 15.9%; --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; + --muted: 250 3.7% 15.9%; + --muted-foreground: 250 5% 64.9%; + --accent: 267 75% 35%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 85.7% 97.3%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; + --border: 250 3.7% 15.9%; + --input: 250 3.7% 15.9%; + --ring: 267 75% 64%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } diff --git a/apps/embed-v1/src/app/layout.tsx b/apps/embed-v1/src/app/layout.tsx index e5bc6a5..dcb17aa 100644 --- a/apps/embed-v1/src/app/layout.tsx +++ b/apps/embed-v1/src/app/layout.tsx @@ -1,9 +1,9 @@ import type { Metadata, Viewport } from "next"; import { GeistMono } from "geist/font/mono"; import { GeistSans } from "geist/font/sans"; +import { ThemeProvider } from "next-themes"; import { cn } from "@acme/ui"; -import { ThemeProvider } from "@acme/ui/theme"; import { Toaster } from "@acme/ui/toast"; import { TRPCReactProvider } from "~/trpc/react"; @@ -50,7 +50,7 @@ export default function RootLayout(props: { children: React.ReactNode }) { GeistMono.variable, )} > - + {props.children} diff --git a/apps/embed-v1/src/app/m/[orgName]/page.tsx b/apps/embed-v1/src/app/m/[orgName]/page.tsx new file mode 100644 index 0000000..9eb6094 --- /dev/null +++ b/apps/embed-v1/src/app/m/[orgName]/page.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +import NotFound from "@acme/ui/components/404-not-found"; + +import { getOrgTestimonials } from "~/actions/getOrgTestimonials"; +import MarqueeMain from "~/components/marquee-main"; +import { ReviewCard } from "~/components/review-card"; + +export default async function TestimonialsPage({ + params, +}: { + params: { orgName: string }; +}) { + const { orgName } = params; + const data = await getOrgTestimonials(orgName); + + if (!data || typeof data === "string") return ; + if (data.testimonials.length === 0) return ; + + if (data.testimonials.length < 4) { + return ( +
+ {data.testimonials.map((testimonial, index) => ( + + ))} +
+ ); + } + + // Split testimonials into 5 columns for marquee effect + const testimonialsData = data.testimonials.reduce( + (acc, testimonial, index) => { + const columnIndex = index % 5; + + if (!acc[columnIndex]) { + acc[columnIndex] = []; + } + + acc[columnIndex].push( + , + ); + return acc; + }, + [], + ); + + return ( + + ); +} diff --git a/apps/embed-v1/src/app/page.tsx b/apps/embed-v1/src/app/page.tsx index e83ea92..8d9bfbf 100644 --- a/apps/embed-v1/src/app/page.tsx +++ b/apps/embed-v1/src/app/page.tsx @@ -1,3 +1,7 @@ export default function HomePage() { - return
=
; + return ( +
+ Nothing to see here +
+ ); } diff --git a/apps/embed-v1/src/components/marquee-main.tsx b/apps/embed-v1/src/components/marquee-main.tsx new file mode 100644 index 0000000..fef161a --- /dev/null +++ b/apps/embed-v1/src/components/marquee-main.tsx @@ -0,0 +1,79 @@ +"use client"; + +import React from "react"; +import { useSearchParams } from "next/navigation"; +import { useTheme } from "next-themes"; + +import { cn } from "@acme/ui"; + +import type { TestimonialType } from "~/types/schema-types"; +import { Marquee } from "./marquee"; +import { ReviewCard } from "./review-card"; + +export default function MarqueeMain({ + testmonials, + data, +}: { + testmonials: JSX.Element[][]; + data: TestimonialType[]; +}) { + const searchParams = useSearchParams(); + const noMarquee = searchParams.get("no-marquee"); + const cantainerClassName = searchParams.get("container-classname"); + const darkTheme = searchParams.get("darktheme"); + + const { theme, setTheme } = useTheme(); + + // http://localhost:3001/m/zenstream?classname=df%20sdf&theme=dark&marquee=%22true%22 + + React.useEffect(() => { + if (darkTheme) { + setTheme("dark"); + } else { + setTheme("light"); + } + }, [theme, setTheme, darkTheme]); + + if (noMarquee) { + return ( + <> +
+ {data.map((item) => ( + + ))} + {data.map((item) => ( + + ))} +
+ + ); + } + + return ( +
+ {testmonials.map((columnData, columnIndex) => ( + + {columnData} + + ))} +
+ ); +} diff --git a/apps/embed-v1/src/components/marquee.tsx b/apps/embed-v1/src/components/marquee.tsx new file mode 100644 index 0000000..5574d0e --- /dev/null +++ b/apps/embed-v1/src/components/marquee.tsx @@ -0,0 +1,51 @@ +import { cn } from "@acme/ui"; + +interface MarqueeProps { + className?: string; + reverse?: boolean; + pauseOnHover?: boolean; + children?: React.ReactNode; + vertical?: boolean; + repeat?: number; + [key: string]: unknown; +} + +export function Marquee({ + className, + reverse, + pauseOnHover = false, + children, + vertical = false, + repeat = 4, + ...props +}: MarqueeProps) { + return ( +
+ {Array(repeat) + .fill(0) + .map((_, i) => ( +
+ {children} +
+ ))} +
+ ); +} diff --git a/apps/embed-v1/src/components/review-card.tsx b/apps/embed-v1/src/components/review-card.tsx new file mode 100644 index 0000000..e2cbf32 --- /dev/null +++ b/apps/embed-v1/src/components/review-card.tsx @@ -0,0 +1,54 @@ +import Image from "next/image"; + +import { cn } from "@acme/ui"; + +import type { TestimonialType } from "~/types/schema-types"; + +export const ReviewCard = ({ + data, + className, +}: { + data: TestimonialType; + className?: string; +}) => { + return ( +
+
+ +
+
+ {data.authorName} +
+

+ {data.authorEmail} +

+
+
+
{data.message}
+
+ {data.reviewImages && ( + + )} +
+
+ ); +}; diff --git a/apps/embed-v1/src/types/schema-types.tsx b/apps/embed-v1/src/types/schema-types.tsx new file mode 100644 index 0000000..20b5d9b --- /dev/null +++ b/apps/embed-v1/src/types/schema-types.tsx @@ -0,0 +1,20 @@ +import type { organizationTable, testimonialTable } from "@acme/db/schema"; + +export interface OrganizationTestimonialType { + id: string; + ownerId: string; + website: string; + logo: string | null; + organizationName: string; + createdAt: Date; + updatedAt: Date; + headerTitle: string; + customMessage: string; + testimonials: TestimonialType[]; +} + +export type OrganizationType = typeof organizationTable.$inferSelect; + +export type TestimonialType = typeof testimonialTable.$inferSelect; + +export type TestimonialTableType = typeof testimonialTable; diff --git a/apps/embed-v1/tailwind.config.ts b/apps/embed-v1/tailwind.config.ts index 17602f6..f3d7063 100644 --- a/apps/embed-v1/tailwind.config.ts +++ b/apps/embed-v1/tailwind.config.ts @@ -14,6 +14,20 @@ export default { sans: ["var(--font-geist-sans)", ...fontFamily.sans], mono: ["var(--font-geist-mono)", ...fontFamily.mono], }, + animation: { + marquee: "marquee var(--duration) linear infinite", + "marquee-vertical": "marquee-vertical var(--duration) linear infinite", + }, + keyframes: { + marquee: { + from: { transform: "translateX(0)" }, + to: { transform: "translateX(calc(-100% - var(--gap)))" }, + }, + "marquee-vertical": { + from: { transform: "translateY(0)" }, + to: { transform: "translateY(calc(-100% - var(--gap)))" }, + }, + }, }, }, } satisfies Config; diff --git a/apps/www/src/app/(main)/(dashborad)/products/[id]/(Settings)/delete-this-space/page.tsx b/apps/www/src/app/(main)/(dashborad)/products/[id]/(Settings)/delete-this-space/page.tsx index 213e487..ca27d0d 100644 --- a/apps/www/src/app/(main)/(dashborad)/products/[id]/(Settings)/delete-this-space/page.tsx +++ b/apps/www/src/app/(main)/(dashborad)/products/[id]/(Settings)/delete-this-space/page.tsx @@ -13,7 +13,7 @@ export default async function page({ params }: { params: { id: string } }) { } return ( -
+
Delete my account
diff --git a/apps/www/src/app/(main)/(dashborad)/products/[id]/(Settings)/edit-this-space/page.tsx b/apps/www/src/app/(main)/(dashborad)/products/[id]/(Settings)/edit-this-space/page.tsx index bbad24c..eb9a4f2 100644 --- a/apps/www/src/app/(main)/(dashborad)/products/[id]/(Settings)/edit-this-space/page.tsx +++ b/apps/www/src/app/(main)/(dashborad)/products/[id]/(Settings)/edit-this-space/page.tsx @@ -13,7 +13,7 @@ export default async function page({ params }: { params: { id: string } }) { } return ( -
+
); diff --git a/apps/www/src/app/(main)/(dashborad)/products/[id]/(embeddings)/embed-doc/page.tsx b/apps/www/src/app/(main)/(dashborad)/products/[id]/(embeddings)/embed-doc/page.tsx new file mode 100644 index 0000000..88d710d --- /dev/null +++ b/apps/www/src/app/(main)/(dashborad)/products/[id]/(embeddings)/embed-doc/page.tsx @@ -0,0 +1,129 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@acme/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@acme/ui/table"; + +export default function TestimonialEmbedDocs() { + const queryParams = [ + { + name: "no-marquee", + type: "boolean", + description: + "When set, displays testimonials in a static grid layout instead of a marquee.", + example: "?no-marquee=true", + }, + { + name: "container-classname", + type: "string", + description: "Applies additional CSS classes to the container div.", + example: "?container-classname=my-custom-class", + }, + { + name: "darktheme", + type: "boolean", + description: "Forces the dark theme when set.", + example: "?darktheme=true", + }, + { + name: "theme", + type: "string", + description: "Sets the theme explicitly. Can be 'light' or 'dark'.", + example: "?theme=dark", + }, + { + name: "marquee", + type: "string", + description: "Controls marquee behavior. Set to 'true' to enable.", + example: '?marquee="true"', + }, + ]; + + return ( +
+ + + Testimonial Embed Documentation + + Customize your testimonial embed using these query parameters + + + + + + + Parameter + Type + Description + Example + + + + {queryParams.map((param) => ( + + {param.name} + {param.type} + {param.description} + + {param.example} + + + ))} + +
+ +
+

Usage Examples

+
    +
  • + http://localhost:3001/m/zenstream?no-marquee=true +

    + Displays testimonials in a static grid layout +

    +
  • +
  • + http://localhost:3001/m/zenstream?darktheme=true +

    + Forces the dark theme +

    +
  • +
  • + + http://localhost:3001/m/zenstream?container-classname=custom-container&theme=dark&marquee="true" + +

    + Applies a custom class, sets dark theme, and enables marquee +

    +
  • +
+
+ +
+

Notes

+
    +
  • Parameters can be combined using the & symbol.
  • +
  • + Boolean parameters are considered true when present, regardless + of their value. +
  • +
  • + The 'theme' parameter takes precedence over 'darktheme' if both + are provided. +
  • +
+
+
+
+
+ ); +} diff --git a/apps/www/src/app/(main)/(dashborad)/products/[id]/(embeddings)/embed/page.tsx b/apps/www/src/app/(main)/(dashborad)/products/[id]/(embeddings)/embed/page.tsx new file mode 100644 index 0000000..3d20cfc --- /dev/null +++ b/apps/www/src/app/(main)/(dashborad)/products/[id]/(embeddings)/embed/page.tsx @@ -0,0 +1,5 @@ +import Embedding from "~/components/embeddings/embedding"; + +export default function page({ params }: { params: { id: string } }) { + return ; +} diff --git a/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/_components/show-case-links.tsx b/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/_components/show-case-links.tsx index 5a52634..5fca490 100644 --- a/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/_components/show-case-links.tsx +++ b/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/_components/show-case-links.tsx @@ -16,7 +16,7 @@ export default function ShowCaseLink({ const domain = document.location.origin; return (
-
On our hosted page
+
On our hosted page
{`${domain}/${name}/${page}`}
diff --git a/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/request-testimonial/page.tsx b/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/request-testimonial/page.tsx index 0b61ef4..8a8092a 100644 --- a/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/request-testimonial/page.tsx +++ b/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/request-testimonial/page.tsx @@ -4,7 +4,7 @@ import ShowCaseLink from "../_components/show-case-links"; export default function page({ params }: { params: { id: string } }) { return ( -
+

Request Testimonial

Share this link with your clients or customers to request testimonials diff --git a/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/wall-of-fame/page.tsx b/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/wall-of-fame/page.tsx index a7a77b8..524c279 100644 --- a/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/wall-of-fame/page.tsx +++ b/apps/www/src/app/(main)/(dashborad)/products/[id]/(pages)/wall-of-fame/page.tsx @@ -4,7 +4,7 @@ import ShowCaseLink from "../_components/show-case-links"; export default function page({ params }: { params: { id: string } }) { return ( -
+

Wall 0f Fame

share you wall of fame
diff --git a/apps/www/src/app/(main)/(dashborad)/products/[id]/layout.tsx b/apps/www/src/app/(main)/(dashborad)/products/[id]/layout.tsx index 54ea282..ae54672 100644 --- a/apps/www/src/app/(main)/(dashborad)/products/[id]/layout.tsx +++ b/apps/www/src/app/(main)/(dashborad)/products/[id]/layout.tsx @@ -17,9 +17,9 @@ export default async function Layout({ } return (
- - -
+ +
+
diff --git a/apps/www/src/components/embeddings/embedding.tsx b/apps/www/src/components/embeddings/embedding.tsx new file mode 100644 index 0000000..1f37049 --- /dev/null +++ b/apps/www/src/components/embeddings/embedding.tsx @@ -0,0 +1,93 @@ +"use client"; + +import React, { useState } from "react"; + +import { Button } from "@acme/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@acme/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@acme/ui/tabs"; + +const HighlightedCode = ({ code }: { code: string }) => ( +
+    {code}
+  
+); + +export default function Embedding({ website }: { website: string }) { + const [showEmbed, setShowEmbed] = useState(true); + + const embedCode = `