├── .eslintrc.json ├── src ├── lib │ ├── page-size.tsx │ ├── providers │ │ ├── form-provider.tsx │ │ ├── keys-context.tsx │ │ ├── reference-context.tsx │ │ ├── pager-context.tsx │ │ ├── editor-status-context.tsx │ │ ├── selection-context.tsx │ │ └── document-provider.tsx │ ├── validation │ │ ├── page-number-schema.tsx │ │ ├── fonts-schema.tsx │ │ ├── element-type.tsx │ │ ├── theme-schema.tsx │ │ ├── brand-schema.tsx │ │ ├── document-schema.tsx │ │ ├── slide-schema.tsx │ │ ├── image-schema.tsx │ │ └── text-schema.tsx │ ├── utils.ts │ ├── hooks │ │ ├── use-keys.tsx │ │ ├── use-media-query.tsx │ │ ├── use-field-array-values.tsx │ │ ├── use-pager.tsx │ │ ├── use-selection.tsx │ │ ├── use-window-dimensions.tsx │ │ ├── use-persist-form.tsx │ │ └── use-fields-file-importer.tsx │ ├── rate-limit.ts │ ├── convert-file.tsx │ ├── field-path.tsx │ ├── text-style-to-classes.ts │ ├── pallettes.tsx │ ├── default-document.tsx │ ├── fonts-map.tsx │ ├── theme-utils.ts │ ├── default-slides.tsx │ ├── document-form-types.tsx │ └── langchain.ts ├── types │ └── index.d.ts ├── app │ ├── favicon.ico │ ├── opengraph-image.png │ ├── page.tsx │ ├── actions.tsx │ ├── api │ │ └── proxy │ │ │ └── route.ts │ ├── globals.css │ └── layout.tsx └── components │ ├── loading-spinner.tsx │ ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── unstyled-input.tsx │ ├── toaster.tsx │ ├── unstyled-textarea.tsx │ ├── checkbox.tsx │ ├── tooltip.tsx │ ├── hover-card.tsx │ ├── slider.tsx │ ├── auto-text-area.tsx │ ├── popover.tsx │ ├── toggle.tsx │ ├── radio-group.tsx │ ├── scroll-area.tsx │ ├── toggle-group.tsx │ ├── button.tsx │ ├── tabs.tsx │ ├── card.tsx │ ├── vertical-tabs.tsx │ ├── drawer.tsx │ ├── dialog.tsx │ ├── use-toast.ts │ ├── select.tsx │ └── form.tsx │ ├── json-exporter.tsx │ ├── elements │ ├── background-layer.tsx │ ├── page-number.tsx │ ├── background-image-layer.tsx │ ├── footer.tsx │ ├── subtitle.tsx │ ├── description.tsx │ ├── signature.tsx │ ├── title.tsx │ └── content-image.tsx │ ├── page-counter.tsx │ ├── color-theme-display.tsx │ ├── no-api-keys-text.tsx │ ├── forms │ ├── fields │ │ ├── image-form-field.tsx │ │ ├── opacity-form-field.tsx │ │ ├── text-area-form-field.tsx │ │ ├── enum-radio-group-field.tsx │ │ └── image-source-form-field.tsx │ ├── page-number-form.tsx │ ├── filename-form.tsx │ ├── brand-form.tsx │ ├── fonts-form.tsx │ └── file-input-form.tsx │ ├── pages │ ├── page-frame.tsx │ ├── page-layout.tsx │ ├── page-base.tsx │ ├── add-element.tsx │ └── new-page.tsx │ ├── editor.tsx │ ├── ai-panel.tsx │ ├── drawer.tsx │ ├── custom-indicator-radio-group-item.tsx │ ├── icons.tsx │ ├── site-footer.tsx │ ├── slide-menubar-wrapper.tsx │ ├── json-importer.tsx │ ├── editor-skeleton.tsx │ ├── github-icon.tsx │ ├── star-on-github.tsx │ ├── new-page-dialog-content.tsx │ ├── pager.tsx │ ├── new-element-dialog-content.tsx │ ├── main-nav.tsx │ ├── ai-textarea-form.tsx │ ├── slide-menubar.tsx │ ├── ai-input-form.tsx │ ├── slides-editor.tsx │ ├── api-keys-dialog.tsx │ └── typography.tsx ├── public ├── laptop_no_bg.png └── fonts │ ├── DM_Sans │ ├── DMSans-Bold.ttf │ └── DMSans-Regular.ttf │ ├── Roboto │ ├── Roboto-Bold.ttf │ ├── Roboto-Medium.ttf │ └── Roboto-Regular.ttf │ ├── Ultra │ └── Ultra-Regular.ttf │ ├── PT_Serif │ ├── PTSerif-Bold.ttf │ └── PTSerif-Regular.ttf │ ├── DM_Serif_Display │ └── DMSerifDisplay-Regular.ttf │ └── Roboto_Condensed │ ├── RobotoCondensed-Bold.ttf │ └── RobotoCondensed-Regular.ttf ├── postcss.config.js ├── next.config.js ├── components.json ├── .env.example ├── .gitignore ├── tsconfig.json ├── LICENSE ├── .vscode ├── launch.json └── settings.json ├── package.json ├── tailwind.config.js └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/page-size.tsx: -------------------------------------------------------------------------------- 1 | export const SIZE = { 2 | width: 400, 3 | height: 500, 4 | }; 5 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // html2pdf doesn't have module declaration 2 | declare module "html2pdf.js"; 3 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/laptop_no_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/laptop_no_bg.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/src/app/opengraph-image.png -------------------------------------------------------------------------------- /public/fonts/DM_Sans/DMSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/DM_Sans/DMSans-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/Roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Ultra/Ultra-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/Ultra/Ultra-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/PT_Serif/PTSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/PT_Serif/PTSerif-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/Roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/Roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/lib/providers/form-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FormProvider } from "react-hook-form"; 4 | 5 | export default FormProvider; 6 | -------------------------------------------------------------------------------- /public/fonts/DM_Sans/DMSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/DM_Sans/DMSans-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/PT_Serif/PTSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/PT_Serif/PTSerif-Regular.ttf -------------------------------------------------------------------------------- /src/lib/validation/page-number-schema.tsx: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const PageNumberSchema = z.object({ 4 | showNumbers: z.boolean(), 5 | }); 6 | -------------------------------------------------------------------------------- /src/lib/validation/fonts-schema.tsx: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const FontsSchema = z.object({ 4 | font1: z.string(), 5 | font2: z.string(), 6 | }); 7 | -------------------------------------------------------------------------------- /public/fonts/DM_Serif_Display/DMSerifDisplay-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/DM_Serif_Display/DMSerifDisplay-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto_Condensed/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/Roboto_Condensed/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto_Condensed/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FranciscoMoretti/carousel-generator/HEAD/public/fonts/Roboto_Condensed/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /src/components/loading-spinner.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2Icon } from "lucide-react"; 2 | 3 | export function LoadingSpinner() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/validation/element-type.tsx: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const ElementType = z.enum([ 4 | "Title", 5 | "Subtitle", 6 | "Description", 7 | "Image", 8 | "ContentImage", 9 | ]); 10 | export type ElementType = z.infer; 11 | -------------------------------------------------------------------------------- /src/lib/hooks/use-keys.tsx: -------------------------------------------------------------------------------- 1 | import { FocusEvent, useState } from "react"; 2 | 3 | export function useKeys() { 4 | const [apiKey, setApiKey] = useState( 5 | process.env.NEXT_PUBLIC_OPENAI_KEY || "" 6 | ); 7 | 8 | return { 9 | apiKey, 10 | setApiKey, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Editor from "@/components/editor"; 4 | import { DocumentProvider } from "@/lib/providers/document-provider"; 5 | 6 | export default function Home() { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/validation/theme-schema.tsx: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const ColorSchema = z.object({ 4 | // primary: z.string().min(4).max(9).regex(/^#/), 5 | primary: z.string().min(7).max(7).regex(/^#/), 6 | secondary: z.string(), 7 | background: z.string(), 8 | }); 9 | 10 | export const ThemeSchema = ColorSchema.extend({ 11 | isCustom: z.boolean(), 12 | pallette: z.string(), 13 | }); 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=http://localhost:3000 2 | 3 | 4 | # Get your OpenAI API key here: https://platform.openai.com/account/api-keys 5 | # Client (for testing) 6 | NEXT_PUBLIC_OPENAI_KEY= 7 | 8 | # Server 9 | OPENAI_API_KEY="" 10 | 11 | # OPTIONAL: Vercel KV (for ratelimiting) 12 | # Get your Vercel KV credentials here: https://vercel.com/docs/storage/vercel-kv/quickstart#quickstart 13 | KV_REST_API_URL="" 14 | KV_REST_API_TOKEN="" 15 | -------------------------------------------------------------------------------- /src/components/json-exporter.tsx: -------------------------------------------------------------------------------- 1 | export function JsonExporter({ 2 | values, 3 | filename, 4 | children, 5 | }: { 6 | values: any; 7 | filename: string; 8 | children: React.ReactNode; 9 | }) { 10 | return ( 11 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/rate-limit.ts: -------------------------------------------------------------------------------- 1 | import { Ratelimit } from "@upstash/ratelimit"; 2 | import { Redis } from "@upstash/redis"; 3 | 4 | 5 | const redis = new Redis({ 6 | url: process.env.KV_REST_API_URL || "", 7 | token: process.env.KV_REST_API_TOKEN || "", 8 | }); 9 | 10 | export const messageRateLimit = new Ratelimit({ 11 | redis, 12 | limiter: Ratelimit.slidingWindow(10, "15 m"), 13 | analytics: true, 14 | prefix: "ratelimit:carousel:msg", 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/validation/brand-schema.tsx: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | import { DEFAULT_IMAGE_INPUT, ImageSchema } from "./image-schema"; 3 | 4 | export const BrandSchema = z.object({ 5 | avatar: ImageSchema.default(DEFAULT_IMAGE_INPUT), 6 | name: z 7 | .string() 8 | .min(2, { 9 | message: "Name be at least 2 characters.", 10 | }) 11 | .max(30, { 12 | message: "Name must not be longer than 30 characters.", 13 | }), 14 | handle: z.string(), 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/convert-file.tsx: -------------------------------------------------------------------------------- 1 | export const convertFileToDataUrl = (file: File): Promise => { 2 | return new Promise((resolve, reject) => { 3 | const reader = new FileReader(); 4 | reader.onload = (event) => { 5 | if (event.target?.result) { 6 | resolve(event.target.result.toString()); 7 | } else { 8 | reject(new Error("Failed to read the file.")); 9 | } 10 | }; 11 | reader.onerror = (error) => reject(error); 12 | reader.readAsDataURL(file); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/elements/background-layer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | export function BackgroundLayer({ 5 | background, 6 | className = "", 7 | }: { 8 | background: string; 9 | className?: string; 10 | }) { 11 | return ( 12 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | .env 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | -------------------------------------------------------------------------------- /src/lib/hooks/use-media-query.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches); 9 | } 10 | 11 | const result = matchMedia(query); 12 | result.addEventListener("change", onChange); 13 | setValue(result.matches); 14 | 15 | return () => result.removeEventListener("change", onChange); 16 | }, [query]); 17 | 18 | return value; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/hooks/use-field-array-values.tsx: -------------------------------------------------------------------------------- 1 | import { UseFormWatch, useFormContext } from "react-hook-form"; 2 | import { DocumentFormReturn } from "../document-form-types"; 3 | 4 | export function useFieldArrayValues(fieldArrayName: string) { 5 | const { watch }: DocumentFormReturn = useFormContext(); // retrieve those props 6 | 7 | // @ts-ignore should be only the available fields. 8 | // TODO: construct argument type 9 | const currentSlidesValues = watch(fieldArrayName); 10 | const numPages: number | undefined = currentSlidesValues?.length; 11 | return { numPages }; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/hooks/use-pager.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { DocumentFormReturn } from "../document-form-types"; 3 | 4 | export function usePager(initialPage: number) { 5 | const [currentPage, _setCurrentPage] = useState(initialPage); 6 | 7 | const onPreviousClick = () => { 8 | _setCurrentPage(currentPage - 1); 9 | }; 10 | 11 | const onNextClick = () => { 12 | _setCurrentPage(currentPage + 1); 13 | }; 14 | 15 | const setCurrentPage = (pageNum: number) => { 16 | _setCurrentPage(pageNum); 17 | }; 18 | 19 | return { 20 | currentPage, 21 | onPreviousClick, 22 | onNextClick, 23 | setCurrentPage, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/field-path.tsx: -------------------------------------------------------------------------------- 1 | export function getStyleSibling(field: string): string { 2 | return getParent(field) + ".style"; 3 | } 4 | 5 | export function getSlideNumber(field: string): number { 6 | if (!field.startsWith("slides")) { 7 | throw Error("Getting slide number of field without slides"); 8 | } 9 | return Number(field.split(".").slice(1, 2)); 10 | } 11 | 12 | export function getElementNumber(field: string): number { 13 | if (!field.startsWith("slides")) { 14 | throw Error("Getting slide number of field without slides"); 15 | } 16 | return Number(field.split(".").slice(3, 4)); 17 | } 18 | 19 | export function getParent(field: string): string { 20 | return field.split(".").slice(0, -1).join("."); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/page-counter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFieldArrayValues } from "@/lib/hooks/use-field-array-values"; 4 | import { usePagerContext } from "@/lib/providers/pager-context"; 5 | import { useEffect, useState } from "react"; 6 | 7 | export function PageCounter() { 8 | const [isClient, setIsClient] = useState(false); 9 | 10 | const { currentPage } = usePagerContext(); 11 | const { numPages } = useFieldArrayValues("slides"); 12 | useEffect(() => { 13 | setIsClient(true); 14 | }, []); 15 | 16 | return ( 17 |

18 | {isClient 19 | ? `Slide ${Math.min(currentPage + 1, numPages)} / ${numPages}` 20 | : ""} 21 |

22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/providers/keys-context.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | interface KeysContextValue { 4 | apiKey: string; 5 | setApiKey: (apiKey: string) => void; 6 | } 7 | 8 | const KeysContext = React.createContext( 9 | undefined 10 | ); 11 | 12 | export function KeysProvider({ 13 | children, 14 | value, 15 | }: { 16 | children: React.ReactNode; 17 | value: KeysContextValue; 18 | }) { 19 | return {children}; 20 | } 21 | 22 | export function useKeysContext() { 23 | const context = useContext(KeysContext); 24 | if (!context) { 25 | throw new Error("useKeysContext must be used within a KeysProvider"); 26 | } 27 | return context; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/hooks/use-selection.tsx: -------------------------------------------------------------------------------- 1 | import { FocusEvent, useState } from "react"; 2 | 3 | export function useSelection() { 4 | const [currentSelection, setCurrentSelection] = useState(""); 5 | 6 | function _setCurrentSelection( 7 | currentSelection: string, 8 | event: 9 | | FocusEvent 10 | | React.MouseEvent 11 | | null 12 | ) { 13 | // Only clear selection if this element started the event 14 | // if (event.target == event.currentTarget) { 15 | setCurrentSelection(currentSelection); 16 | if (event) event.stopPropagation(); 17 | // } 18 | } 19 | 20 | return { 21 | currentSelection, 22 | setCurrentSelection: _setCurrentSelection, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/validation/document-schema.tsx: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | import { MultiSlideSchema, UnstyledMultiSlideSchema } from "./slide-schema"; 3 | import { ThemeSchema } from "./theme-schema"; 4 | import { BrandSchema } from "./brand-schema"; 5 | import { FontsSchema } from "./fonts-schema"; 6 | import { PageNumberSchema } from "./page-number-schema"; 7 | 8 | export const ConfigSchema = z.object({ 9 | brand: BrandSchema, 10 | theme: ThemeSchema, 11 | fonts: FontsSchema, 12 | pageNumber: PageNumberSchema, 13 | }); 14 | 15 | export const DocumentSchema = z.object({ 16 | slides: MultiSlideSchema, 17 | config: ConfigSchema, 18 | filename: z.string(), 19 | }); 20 | 21 | export const UnstyledDocumentSchema = z.object({ 22 | slides: UnstyledMultiSlideSchema, 23 | }); 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | "src/index.js" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /src/components/color-theme-display.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as React from "react"; 3 | import { Colors } from "@/lib/pallettes"; 4 | 5 | export function ColorThemeDisplay({ colors }: { colors: Colors }) { 6 | return ( 7 |
8 | 14 | 20 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/app/actions.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { messageRateLimit } from "@/lib/rate-limit"; 3 | 4 | import { generateCarouselSlides } from "@/lib/langchain"; 5 | import { headers } from "next/headers"; 6 | 7 | export async function generateCarouselSlidesAction(userPrompt: string) { 8 | if (!process.env.OPENAI_API_KEY) { 9 | return null; 10 | } 11 | 12 | if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { 13 | const ip = headers().get("x-real-ip") ?? "local"; 14 | const rl = await messageRateLimit.limit(ip); 15 | 16 | if (!rl.success) { 17 | // TODO: Handle returning errors 18 | return null; 19 | } 20 | } 21 | 22 | const generatedSlides = await generateCarouselSlides( 23 | userPrompt, 24 | process.env.OPENAI_API_KEY 25 | ); 26 | return generatedSlides; 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/providers/reference-context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useRef, ReactNode } from "react"; 2 | 3 | // Create a context for the ref 4 | const RefContext = createContext | null>( 5 | null 6 | ); 7 | 8 | // Custom hook to access the ref value 9 | export function useRefContext() { 10 | const context = useContext(RefContext); 11 | if (!context) { 12 | throw new Error("useRefContext must be used within a RefProvider"); 13 | } 14 | return context; 15 | } 16 | 17 | // The RefProvider component 18 | interface RefProviderProps { 19 | children: ReactNode; 20 | myRef: React.RefObject; 21 | } 22 | 23 | export function RefProvider({ children, myRef }: RefProviderProps) { 24 | return {children}; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/elements/page-number.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ConfigSchema } from "@/lib/validation/document-schema"; 3 | import { z } from "zod"; 4 | import { cn } from "@/lib/utils"; 5 | import { fontIdToClassName } from "@/lib/fonts-map"; 6 | 7 | export function PageNumber({ 8 | config, 9 | number, 10 | className, 11 | }: { 12 | config: z.infer; 13 | number: number; 14 | className?: string; 15 | }) { 16 | // TODO: Use the view to optionally add a circle around it 17 | return ( 18 |
19 |

25 | {number} 26 |

27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/providers/pager-context.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | interface PagerContextValue { 4 | currentPage: number; 5 | onPreviousClick: () => void; 6 | onNextClick: () => void; 7 | setCurrentPage: (pageNum: number) => void; 8 | } 9 | 10 | const PagerContext = React.createContext( 11 | undefined 12 | ); 13 | 14 | export function PagerProvider({ 15 | children, 16 | value, 17 | }: { 18 | children: React.ReactNode; 19 | value: PagerContextValue; 20 | }) { 21 | return ( 22 | {children} 23 | ); 24 | } 25 | 26 | export function usePagerContext() { 27 | const context = useContext(PagerContext); 28 | if (!context) { 29 | throw new Error("usePagerContext must be used within a PagerProvider"); 30 | } 31 | return context; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |