├── .prettierrc ├── convex.json ├── src ├── vite-env.d.ts ├── components │ ├── Code.tsx │ ├── SignIn │ │ ├── SignInMethodDivider.tsx │ │ ├── SignInDialog.tsx │ │ └── SignInForm.tsx │ ├── Toolbar.tsx │ ├── Markdown.tsx │ ├── ThemeToggle.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── toaster.tsx │ │ ├── switch.tsx │ │ ├── toggle.tsx │ │ ├── scroll-area.tsx │ │ ├── toggle-group.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── use-toast.ts │ │ ├── form.tsx │ │ ├── toast.tsx │ │ └── dropdown-menu.tsx │ ├── PageTitle.tsx │ ├── Search.tsx │ ├── GetStarted │ │ ├── ConvexLogo.tsx │ │ └── GetStartedDialog.tsx │ ├── UserMenu.tsx │ ├── Author │ │ ├── Profile.tsx │ │ └── Edit.tsx │ ├── PageLayout.tsx │ ├── Blog │ │ ├── History.tsx │ │ ├── Post.tsx │ │ └── Edit.tsx │ └── Inputs.tsx ├── pages │ ├── __layout │ │ ├── about.tsx │ │ ├── __authed │ │ │ ├── new.tsx │ │ │ ├── authors.$user.edit.tsx │ │ │ └── $slug.edit.tsx │ │ ├── authors │ │ │ ├── index.tsx │ │ │ └── $user.tsx │ │ ├── __authed.tsx │ │ ├── index.tsx │ │ └── $slug.tsx │ └── __layout.tsx ├── main.tsx ├── index.css └── lib │ └── utils.ts ├── postcss.config.js ├── convex ├── auth.config.ts ├── crons.ts ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── http.ts ├── tsconfig.json ├── auth.ts ├── sharp.ts ├── versions.ts ├── images.ts ├── users.ts ├── README.md ├── schema.ts ├── posts.ts └── data.ts ├── tsconfig.json ├── vercel.json ├── .gitignore ├── tsconfig.node.json ├── components.json ├── vite.config.ts ├── tsconfig.app.json ├── README.md ├── .github └── workflows │ └── github-pages.yml ├── public └── assets │ └── convex.svg ├── .eslintrc.cjs ├── index.html ├── package.json ├── tailwind.config.js ├── setup.mjs └── LICENSE.txt /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /convex.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "externalPackages": [ 4 | "sharp" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | domain: process.env.CONVEX_SITE_URL, 5 | applicationID: "convex", 6 | }, 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/cms-demo(.*)", 5 | "destination": "/cms-demo" 6 | }, 7 | { 8 | "source": "/(.*)", 9 | "destination": "/" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Code.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export const Code = ({ children }: { children: ReactNode }) => { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /convex/crons.ts: -------------------------------------------------------------------------------- 1 | import { cronJobs } from "convex/server"; 2 | import { internal } from "./_generated/api"; 3 | 4 | const crons = cronJobs(); 5 | 6 | crons.daily( 7 | "Reset all tables to initial dataset", 8 | { hourUTC: 7, minuteUTC: 0 }, 9 | internal.data.reset, 10 | ); 11 | 12 | export default crons; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/__layout/about.tsx: -------------------------------------------------------------------------------- 1 | import { GetStartedContent } from "@/components/GetStarted/GetStartedDialog"; 2 | import { PageTitle } from "@/components/PageTitle"; 3 | 4 | export default function AboutPage() { 5 | return ( 6 |
7 | 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/SignIn/SignInMethodDivider.tsx: -------------------------------------------------------------------------------- 1 | export function SignInMethodDivider() { 2 | return ( 3 |
4 |
5 | 6 |
7 |
8 | 9 | Or continue with 10 | 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import { Authenticated } from "convex/react"; 2 | import type { ReactNode } from "react"; 3 | 4 | export function Toolbar({ children }: { children?: ReactNode }) { 5 | return ( 6 |
7 | 8 |
9 |
{children}
10 |
11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/__layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import { Toaster } from "@/components/ui/toaster"; 3 | import { Footer, Header } from "@/components/PageLayout"; 4 | 5 | export default function Layout() { 6 | return ( 7 |
8 |
9 |
10 | 11 | 12 |
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | import Pages from "vite-plugin-pages"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), Pages({ routeStyle: "remix" })], 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "./src"), 12 | }, 13 | }, 14 | esbuild: { 15 | platform: 'node', 16 | }, 17 | build: { 18 | sourcemap: true, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/pages/__layout/__authed/new.tsx: -------------------------------------------------------------------------------- 1 | import { Authenticated, Unauthenticated } from "convex/react"; 2 | 3 | import { EditablePost } from "@/components/Blog/Edit"; 4 | 5 | export default function NewPostPage() { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | {/* Should never get here, but just in case */} 13 | Sign in as an editor to access this page 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated `api` utility. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import { anyApi } from "convex/server"; 14 | 15 | /** 16 | * A utility for referencing Convex functions in your app's API. 17 | * 18 | * Usage: 19 | * ```js 20 | * const myFunctionReference = api.myModule.myFunction; 21 | * ``` 22 | */ 23 | export const api = anyApi; 24 | export const internal = anyApi; 25 | 26 | /* prettier-ignore-end */ 27 | -------------------------------------------------------------------------------- /src/pages/__layout/authors/index.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "convex/react"; 2 | import { api } from "../../../../convex/_generated/api"; 3 | import { Message, PageTitle } from "@/components/PageTitle"; 4 | import { AuthorsList } from "@/components/Author/Profile"; 5 | 6 | export default function AuthorsPage() { 7 | const users = useQuery(api.users.listAuthors); 8 | 9 | return ( 10 | <> 11 |
12 | 13 |
14 | {users === undefined ? ( 15 | 16 | ) : ( 17 | 18 | )} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /convex/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRouter } from "convex/server"; 2 | import { auth } from "./auth"; 3 | import { httpAction } from "./_generated/server"; 4 | 5 | const http = httpRouter(); 6 | 7 | auth.addHttpRoutes(http); 8 | 9 | http.route({ 10 | pathPrefix: "/", 11 | method: "GET", 12 | handler: httpAction(async (_, request) => { 13 | const { 14 | body: { storageId }, 15 | } = await request.json(); 16 | 17 | console.log(`received request.url ${request.url}`); 18 | console.log(`received request.body.storageId ${storageId}`); 19 | 20 | return new Response(null, { 21 | status: 200, 22 | }); 23 | }), 24 | }); 25 | 26 | export default http; 27 | -------------------------------------------------------------------------------- /src/components/SignIn/SignInDialog.tsx: -------------------------------------------------------------------------------- 1 | import { SignInForm } from "./SignInForm"; 2 | import { Button } from "../ui/button"; 3 | import { 4 | Dialog, 5 | DialogClose, 6 | DialogContent, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from "../ui/dialog"; 10 | 11 | export function SignInDialog() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | Sign in or create an account 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import Markdown from "react-markdown"; 3 | import rehypeHighlight from "rehype-highlight"; 4 | import remarkGfm from "remark-gfm"; 5 | 6 | export function StyledContent({ children }: { children: ReactNode }) { 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | } 13 | 14 | export function StyledMarkdown({ content }: { content: string }) { 15 | return ( 16 | 17 | 18 | {content} 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/__layout/__authed.tsx: -------------------------------------------------------------------------------- 1 | import { Authenticated, AuthLoading, Unauthenticated } from "convex/react"; 2 | import { Navigate, Outlet, useLocation } from "react-router-dom"; 3 | 4 | export default function Authed() { 5 | return ( 6 | <> 7 | 8 |

Loading...

9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | function GoBack() { 21 | const { pathname } = useLocation(); 22 | const segments = pathname.split("/"); 23 | const previous = segments.slice(0, -1).join("/"); 24 | return ; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; 4 | import { DesktopIcon, MoonIcon, SunIcon } from "@radix-ui/react-icons"; 5 | import { useTheme } from "next-themes"; 6 | 7 | export function ThemeToggle() { 8 | const { theme, setTheme } = useTheme(); 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | "moduleResolution": "Bundler", 11 | "jsx": "react-jsx", 12 | "skipLibCheck": true, 13 | "allowSyntheticDefaultImports": true, 14 | /* These compiler options are required by Convex */ 15 | "target": "ESNext", 16 | "lib": ["ES2021", "dom"], 17 | "forceConsistentCasingInFileNames": true, 18 | "module": "ESNext", 19 | "isolatedModules": true, 20 | "noEmit": true 21 | }, 22 | "include": ["./**/*"], 23 | "exclude": ["./_generated"] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | 4 | export function PageTitle({ 5 | title, 6 | tagline, 7 | }: { 8 | title: string; 9 | tagline?: string; 10 | }) { 11 | const location = useLocation; 12 | useEffect(() => { 13 | document.title = `Convex Blog${title ? ` | ${title}` : ""}`; 14 | }, [location, title]); 15 | 16 | return ( 17 |
18 | {title && ( 19 |

20 | {title} 21 |

22 | )} 23 | {tagline && ( 24 |

{tagline}

25 | )} 26 |
27 | ); 28 | } 29 | 30 | export function Message({ text }: { text: string }) { 31 | return

{text}

; 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 |