├── .vercelignore ├── reset.d.ts ├── src ├── constants │ ├── misc.ts │ ├── base-url.ts │ ├── stripe-subscription.ts │ └── stripe-products.ts ├── lib │ ├── api.ts │ ├── redis.ts │ ├── firecrawl.ts │ ├── qstash.ts │ ├── validators.ts │ ├── stripe │ │ └── client.ts │ ├── vector.ts │ ├── chat-limiter.ts │ ├── openrouter.ts │ ├── hooks │ │ └── use-debounce.ts │ ├── auth-client.ts │ ├── client.ts │ ├── lexical-plugins │ │ ├── multiple-editor-plugin.tsx │ │ └── sync-plugin.tsx │ ├── merge-button-refs.ts │ ├── realtime.ts │ ├── use-debounce.ts │ ├── xml-prompt.ts │ ├── placeholder-plugin.ts │ ├── s3.ts │ ├── diff-utils.ts │ ├── blog-query.ts │ ├── initial-content-plugin.ts │ ├── poll-tweet-status.ts │ ├── email.ts │ └── register-response-hooks.ts ├── db │ ├── schema │ │ ├── index.ts │ │ ├── knowledge.ts │ │ └── auth.ts │ └── index.ts ├── app │ ├── api │ │ ├── auth │ │ │ └── [...all] │ │ │ │ └── route.ts │ │ ├── [[...route]] │ │ │ └── route.ts │ │ └── realtime │ │ │ └── route.ts │ ├── testimonials.tsx │ ├── (auth) │ │ ├── verify │ │ │ └── [token] │ │ │ │ └── route.ts │ │ ├── sign-up │ │ │ └── page.tsx │ │ └── sign-in │ │ │ └── page.tsx │ ├── studio │ │ ├── posted │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── topic-monitor │ │ │ ├── empty-state.tsx │ │ │ └── tweet-body.tsx │ │ ├── scheduled │ │ │ └── page.tsx │ │ ├── feed │ │ │ ├── quoted-tweet.tsx │ │ │ └── tweet-body.tsx │ │ ├── page.tsx │ │ └── knowledge │ │ │ └── memories-tab.tsx │ ├── sitemap.ts │ ├── invite │ │ ├── page.tsx │ │ └── success │ │ │ └── page.tsx │ ├── robots.ts │ ├── layout.tsx │ └── onboarding │ │ └── multi-select.tsx ├── types │ ├── message.ts │ ├── content-reference.ts │ └── post.ts ├── frontend │ └── studio │ │ ├── lib │ │ └── validators.ts │ │ └── components │ │ ├── icons.tsx │ │ ├── modal.tsx │ │ ├── TestimonialsMarquee.tsx │ │ └── confetti.tsx ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── settings-container.tsx │ │ ├── settings-section.tsx │ │ ├── textarea.tsx │ │ ├── github-star-button.tsx │ │ ├── separator.tsx │ │ ├── progress.tsx │ │ ├── collapsible.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── settings-field.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── scroll-button.tsx │ │ ├── checkbox.tsx │ │ ├── toggle.tsx │ │ ├── chat-container.tsx │ │ ├── popover.tsx │ │ ├── badge.tsx │ │ ├── alert.tsx │ │ ├── resizable.tsx │ │ ├── text-shimmer.tsx │ │ ├── duolingo-textarea.tsx │ │ ├── tooltip.tsx │ │ ├── tabs.tsx │ │ ├── slider.tsx │ │ ├── button.tsx │ │ ├── accordion.tsx │ │ ├── duolingo-checkbox.tsx │ │ ├── duolingo-badge.tsx │ │ ├── code-block.tsx │ │ ├── duolingo-radio.tsx │ │ ├── card.tsx │ │ └── breadcrumb.tsx │ ├── ai-elements │ │ ├── response.tsx │ │ └── loader.tsx │ ├── prose.tsx │ ├── tweet-editor │ │ ├── tweet-editor.tsx │ │ ├── tweet.tsx │ │ └── link-preview.tsx │ ├── senja-widget.tsx │ ├── providers │ │ ├── providers.tsx │ │ └── dashboard-providers.tsx │ ├── footer.tsx │ ├── chat │ │ └── loading-message.tsx │ ├── container.tsx │ ├── account-connection │ │ └── index.tsx │ ├── logo-cloud.tsx │ └── whats-new-modal.tsx ├── hooks │ ├── use-tweet-metdata.tsx │ ├── use-mobile.ts │ ├── sidebar-ctx.tsx │ ├── use-media-query.ts │ ├── use-confetti.tsx │ ├── use-editors.tsx │ └── use-chat.tsx ├── server │ ├── routers │ │ ├── chat │ │ │ └── tools │ │ │ │ └── shared.ts │ │ ├── utils │ │ │ ├── get-scheduled-tweet-count.ts │ │ │ ├── get-tweet.ts │ │ │ └── get-account.ts │ │ ├── email-router.ts │ │ └── tweet │ │ │ └── fetch-media-from-s3.ts │ ├── index.ts │ └── jstack.ts └── middleware.ts ├── bun.lockb ├── public ├── jo.jpg ├── josh.jpg ├── banner.png ├── cover.png ├── favicon.ico ├── images │ ├── demo.png │ ├── og-image.png │ ├── user │ │ ├── g_128.jpg │ │ ├── iza_128.jpg │ │ ├── ray_128.jpg │ │ ├── Vikas_128.jpg │ │ ├── aasim_128.jpg │ │ ├── abdush_128.jpg │ │ ├── adam_128.jpg │ │ ├── ahmet_128.jpg │ │ ├── ahmet_128.png │ │ ├── bilal_128.jpg │ │ ├── chris_128.png │ │ ├── daniel_128.jpg │ │ ├── fynn_128.jpg │ │ ├── justin_128.png │ │ ├── nikita_128.jpg │ │ ├── nizzy_128.jpg │ │ ├── rohit_128.png │ │ ├── vlad_128.jpg │ │ ├── vladan_128.png │ │ ├── dominik_128.jpg │ │ ├── michael_128.jpg │ │ ├── muhammad_128.jpg │ │ └── miaugladiator11_128.jpg │ ├── square-og-image.png │ ├── image-example-image-editor.png │ └── logo-bg.svg ├── logo │ ├── upstash.png │ ├── zerodotemail.svg │ └── v0.svg ├── new-monitor.png ├── gifs │ ├── typing-black-cat.gif │ └── typing-grey-cat.gif ├── pattern │ ├── zigzag.svg │ ├── stripes.svg │ ├── graphpaper.svg │ └── waves.svg └── noise.svg ├── .prettierrc ├── jobs └── transcription │ ├── .npmignore │ ├── .gitignore │ ├── jest.config.js │ ├── README.md │ ├── test │ └── lambda.test.ts │ ├── package.json │ ├── tsconfig.json │ ├── bin │ └── lambda.ts │ └── lib │ └── lambda-stack.ts ├── postcss.config.mjs ├── drizzle.config.ts ├── instrumentation-client.ts ├── components.json ├── .gitignore ├── tsconfig.json ├── environment.d.ts ├── .env.example ├── next.config.ts ├── .cursor └── rules │ └── posthog-integration.mdc └── README.md /.vercelignore: -------------------------------------------------------------------------------- 1 | jobs/ -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset' 2 | -------------------------------------------------------------------------------- /src/constants/misc.ts: -------------------------------------------------------------------------------- 1 | export const GITHUB_REPO = 'joschan21/contentport' -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/bun.lockb -------------------------------------------------------------------------------- /public/jo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/jo.jpg -------------------------------------------------------------------------------- /src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import { client } from "./client" 2 | 3 | export const api = client -------------------------------------------------------------------------------- /public/josh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/josh.jpg -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/banner.png -------------------------------------------------------------------------------- /public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/cover.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /public/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/demo.png -------------------------------------------------------------------------------- /public/logo/upstash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/logo/upstash.png -------------------------------------------------------------------------------- /public/new-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/new-monitor.png -------------------------------------------------------------------------------- /src/db/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth" 2 | export * from "./tweet" 3 | export * from "./knowledge" -------------------------------------------------------------------------------- /src/lib/redis.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from "@upstash/redis" 2 | 3 | export const redis = Redis.fromEnv() 4 | -------------------------------------------------------------------------------- /public/images/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/og-image.png -------------------------------------------------------------------------------- /public/images/user/g_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/g_128.jpg -------------------------------------------------------------------------------- /jobs/transcription/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /public/images/user/iza_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/iza_128.jpg -------------------------------------------------------------------------------- /public/images/user/ray_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/ray_128.jpg -------------------------------------------------------------------------------- /public/gifs/typing-black-cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/gifs/typing-black-cat.gif -------------------------------------------------------------------------------- /public/gifs/typing-grey-cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/gifs/typing-grey-cat.gif -------------------------------------------------------------------------------- /public/images/square-og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/square-og-image.png -------------------------------------------------------------------------------- /public/images/user/Vikas_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/Vikas_128.jpg -------------------------------------------------------------------------------- /public/images/user/aasim_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/aasim_128.jpg -------------------------------------------------------------------------------- /public/images/user/abdush_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/abdush_128.jpg -------------------------------------------------------------------------------- /public/images/user/adam_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/adam_128.jpg -------------------------------------------------------------------------------- /public/images/user/ahmet_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/ahmet_128.jpg -------------------------------------------------------------------------------- /public/images/user/ahmet_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/ahmet_128.png -------------------------------------------------------------------------------- /public/images/user/bilal_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/bilal_128.jpg -------------------------------------------------------------------------------- /public/images/user/chris_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/chris_128.png -------------------------------------------------------------------------------- /public/images/user/daniel_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/daniel_128.jpg -------------------------------------------------------------------------------- /public/images/user/fynn_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/fynn_128.jpg -------------------------------------------------------------------------------- /public/images/user/justin_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/justin_128.png -------------------------------------------------------------------------------- /public/images/user/nikita_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/nikita_128.jpg -------------------------------------------------------------------------------- /public/images/user/nizzy_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/nizzy_128.jpg -------------------------------------------------------------------------------- /public/images/user/rohit_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/rohit_128.png -------------------------------------------------------------------------------- /public/images/user/vlad_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/vlad_128.jpg -------------------------------------------------------------------------------- /public/images/user/vladan_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/vladan_128.png -------------------------------------------------------------------------------- /public/images/user/dominik_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/dominik_128.jpg -------------------------------------------------------------------------------- /public/images/user/michael_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/michael_128.jpg -------------------------------------------------------------------------------- /public/images/user/muhammad_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/muhammad_128.jpg -------------------------------------------------------------------------------- /public/images/user/miaugladiator11_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/user/miaugladiator11_128.jpg -------------------------------------------------------------------------------- /public/images/image-example-image-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschan21/contentport/HEAD/public/images/image-example-image-editor.png -------------------------------------------------------------------------------- /jobs/transcription/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /src/lib/firecrawl.ts: -------------------------------------------------------------------------------- 1 | import FirecrawlApp from '@mendable/firecrawl-js' 2 | 3 | export const firecrawl = new FirecrawlApp({ 4 | apiKey: process.env.FIRECRAWL_API_KEY, 5 | }) 6 | -------------------------------------------------------------------------------- /src/lib/qstash.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { Client } from '@upstash/qstash' 3 | 4 | export const qstash = new Client({ 5 | token: process.env.QSTASH_TOKEN, 6 | }) 7 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/lib/auth" 2 | import { toNextJsHandler } from "better-auth/next-js" 3 | 4 | export const { POST, GET } = toNextJsHandler(auth) -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/neon-http" 2 | import * as schema from "./schema" 3 | 4 | export const db = drizzle(process.env.DATABASE_URL ?? "", { schema }) 5 | -------------------------------------------------------------------------------- /src/lib/validators.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const tweet = z.object({ 4 | id: z.string(), 5 | content: z.string(), 6 | }) 7 | 8 | export type Tweet = z.infer 9 | -------------------------------------------------------------------------------- /src/lib/stripe/client.ts: -------------------------------------------------------------------------------- 1 | import { Stripe } from 'stripe' 2 | 3 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 4 | apiVersion: '2025-08-27.basil', 5 | typescript: true, 6 | }) 7 | -------------------------------------------------------------------------------- /src/lib/vector.ts: -------------------------------------------------------------------------------- 1 | import { Index } from '@upstash/vector' 2 | 3 | export const vector = new Index({ 4 | url: process.env.UPSTASH_VECTOR_REST_URL, 5 | token: process.env.UPSTASH_VECTOR_REST_TOKEN, 6 | }) 7 | -------------------------------------------------------------------------------- /src/types/message.ts: -------------------------------------------------------------------------------- 1 | import { UIMessage, UserContent } from 'ai' 2 | 3 | export type TestUIMessage = Omit & { 4 | content: UserContent 5 | metadata?: Record 6 | } 7 | -------------------------------------------------------------------------------- /jobs/transcription/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/chat-limiter.ts: -------------------------------------------------------------------------------- 1 | import { Ratelimit } from "@upstash/ratelimit" 2 | import { redis } from "./redis" 3 | 4 | export const chatLimiter = new Ratelimit({ 5 | redis, 6 | limiter: Ratelimit.fixedWindow(20, "1d"), 7 | }) 8 | -------------------------------------------------------------------------------- /public/pattern/zigzag.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { type Config } from "drizzle-kit" 2 | 3 | export default { 4 | schema: "./src/db/schema", 5 | dialect: "postgresql", 6 | dbCredentials: { 7 | url: process.env.DATABASE_URL!, 8 | }, 9 | out: "./migrations", 10 | } satisfies Config 11 | -------------------------------------------------------------------------------- /public/pattern/stripes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/api/[[...route]]/route.ts: -------------------------------------------------------------------------------- 1 | import appRouter from '@/server' 2 | import { handle } from 'hono/vercel' 3 | 4 | // This route catches all incoming API requests and lets your appRouter handle them. 5 | export const GET = handle(appRouter.handler) 6 | export const POST = handle(appRouter.handler) 7 | -------------------------------------------------------------------------------- /public/logo/zerodotemail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/frontend/studio/lib/validators.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const TWITTER_HANDLE_VALIDATOR = z.object({ 4 | handle: z 5 | .string() 6 | .min(1, { message: "Twitter handle is required" }) 7 | .transform((val) => val.replace(/^@/, "")), 8 | }) 9 | 10 | export type TwitterHandleForm = z.infer 11 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /public/noise.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/hooks/use-tweet-metdata.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | interface TweetMetadata { 4 | 5 | charCount: number 6 | setCharCount: (charCount: number) => void 7 | } 8 | 9 | const useTweetMetadata = create((set) => ({ 10 | 11 | charCount: 0, 12 | setCharCount: (charCount) => set({ charCount }), 13 | })) 14 | 15 | export default useTweetMetadata 16 | -------------------------------------------------------------------------------- /src/frontend/studio/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Check, 3 | InfoIcon, 4 | Link, 5 | Loader2, 6 | MessageSquareHeart, 7 | Plus, 8 | Minus, 9 | } from "lucide-react" 10 | 11 | export const Icons = { 12 | figma: { 13 | check: Check, 14 | link: Link, 15 | infoCircle: InfoIcon, 16 | heartMessage: MessageSquareHeart, 17 | plus: Plus, 18 | minus: Minus, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ui/settings-container.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { ReactNode } from "react" 3 | 4 | interface SettingsContainerProps { 5 | children: ReactNode 6 | className?: string 7 | } 8 | 9 | export function SettingsContainer({ children, className }: SettingsContainerProps) { 10 | return ( 11 |
12 | {children} 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/constants/base-url.ts: -------------------------------------------------------------------------------- 1 | export const getBaseUrl = () => { 2 | if (typeof window !== 'undefined') return window.location.origin 3 | 4 | if (process.env.VERCEL_ENV === 'preview') { 5 | return `https://staging.contentport.io` 6 | } 7 | 8 | if (process.env.NODE_ENV === 'production' && !Boolean(process.env.IS_LOCAL)) { 9 | return `https://contentport.io` 10 | } 11 | 12 | return `http://localhost:3000` 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/openrouter.ts: -------------------------------------------------------------------------------- 1 | import { createOpenRouter } from "@openrouter/ai-sdk-provider"; 2 | 3 | const apiKey = process.env.OPENROUTER_API_KEY; 4 | 5 | if (!apiKey) { 6 | throw new Error("OPENROUTER_API_KEY must be set for AI to work."); 7 | } 8 | 9 | const headers = { 10 | "HTTP-Referer": "https://contentport.io/", 11 | "X-Title": "Contentport", 12 | }; 13 | 14 | export const openrouter = createOpenRouter({ 15 | apiKey, 16 | headers, 17 | }); -------------------------------------------------------------------------------- /src/lib/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export function useDebounce(value: T, delay: number): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => { 8 | setDebouncedValue(value) 9 | }, delay) 10 | 11 | return () => { 12 | clearTimeout(timer) 13 | } 14 | }, [value, delay]) 15 | 16 | return debouncedValue 17 | } -------------------------------------------------------------------------------- /instrumentation-client.ts: -------------------------------------------------------------------------------- 1 | import posthog from 'posthog-js' 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 5 | api_host: '/ingest', 6 | ui_host: 'https://eu.posthog.com', 7 | defaults: '2025-05-24', 8 | capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this 9 | // debug: process.env.NODE_ENV === "development", 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/constants/stripe-subscription.ts: -------------------------------------------------------------------------------- 1 | export interface StripeSubscriptionData { 2 | id: string | null 3 | priceId?: string | null 4 | } 5 | 6 | export const STRIPE_SUBSCRIPTION_DATA: StripeSubscriptionData = { 7 | id: 8 | process.env.NODE_ENV === 'production' ? 'prod_Sdai9fYhooL16t' : 'prod_SdTUFyIfmC3dGO', 9 | priceId: 10 | process.env.NODE_ENV === 'production' 11 | ? 'price_1RiJX1A19umTXGu8k9V4fMkn' 12 | : 'price_1RiCXfA19umTXGu8XAnmgBvK', 13 | } 14 | -------------------------------------------------------------------------------- /src/app/testimonials.tsx: -------------------------------------------------------------------------------- 1 | export const Testimonials = () => { 2 | return ( 3 | <> 4 | 9 |
15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from 'better-auth/react' 2 | import { inferAdditionalFields, magicLinkClient, lastLoginMethodClient } from 'better-auth/client/plugins' 3 | 4 | export const authClient = createAuthClient({ 5 | plugins: [ 6 | lastLoginMethodClient(), 7 | magicLinkClient(), 8 | inferAdditionalFields({ 9 | user: { 10 | plan: { type: 'string', defaultValue: 'free' }, 11 | isAdmin: { type: 'boolean', defaultValue: false }, 12 | }, 13 | }), 14 | ], 15 | }) 16 | -------------------------------------------------------------------------------- /src/lib/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'jstack' 2 | import type { AppRouter } from '@/server' 3 | 4 | /** 5 | * Your type-safe API client 6 | * @see https://jstack.app/docs/backend/api-client 7 | */ 8 | export const client = createClient({ 9 | baseUrl: `${getBaseUrl()}/api`, 10 | credentials: 'include', 11 | }) 12 | 13 | function getBaseUrl() { 14 | if (typeof window !== 'undefined') return window.location.origin 15 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}` 16 | return `http://localhost:3000` 17 | } 18 | -------------------------------------------------------------------------------- /src/app/(auth)/verify/[token]/route.ts: -------------------------------------------------------------------------------- 1 | import { redis } from '@/lib/redis' 2 | import { redirect } from 'next/navigation' 3 | import { NextRequest } from 'next/server' 4 | 5 | interface RouteParams { 6 | params: Promise<{ 7 | token: string 8 | }> 9 | } 10 | 11 | export const GET = async (req: NextRequest, { params }: RouteParams) => { 12 | const { token } = await params 13 | 14 | const redirectUrl = await redis.get(`redirect:${token}`) 15 | 16 | if (!redirectUrl) { 17 | return redirect('/sign-in?expired=true') 18 | } 19 | 20 | return redirect(redirectUrl) 21 | } 22 | -------------------------------------------------------------------------------- /jobs/transcription/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `npx cdk deploy` deploy this stack to your default AWS account/region 13 | * `npx cdk diff` compare deployed stack with current state 14 | * `npx cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /src/app/studio/posted/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Container } from '@/components/container' 4 | import TweetList from '@/components/tweet-list' 5 | import { AccountAvatar } from '@/hooks/account-ctx' 6 | import { CheckCircle2 } from 'lucide-react' 7 | 8 | export default function PostedTweetsPage() { 9 | return ( 10 | 14 |
15 | 16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": { 22 | "@ai-elements": "https://registry.ai-sdk.dev/{name}.json" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/types/content-reference.ts: -------------------------------------------------------------------------------- 1 | import { KnowledgeDocument } from "@/db/schema" 2 | 3 | interface BaseContentReference { 4 | id: string 5 | title: string 6 | type: KnowledgeDocument["type"] 7 | sourceUrl?: string | null 8 | metadata?: Record 9 | } 10 | 11 | export interface Attachment extends BaseContentReference { 12 | lifecycle: "session" 13 | uploadStatus?: "pending" | "uploading" | "completed" | "failed" 14 | file?: File 15 | } 16 | 17 | interface KnowledgeDoc extends BaseContentReference { 18 | lifecycle: "persistent" 19 | } 20 | 21 | export type ContentReference = Attachment | KnowledgeDoc 22 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /jobs/transcription/test/lambda.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as Lambda from '../lib/lambda-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/lambda-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new Lambda.LambdaStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/ai-elements/response.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { type ComponentProps, memo } from "react"; 5 | import { Streamdown } from "streamdown"; 6 | 7 | type ResponseProps = ComponentProps; 8 | 9 | export const Response = memo( 10 | ({ className, ...props }: ResponseProps) => ( 11 | *:first-child]:mt-0 [&>*:last-child]:mb-0", 14 | className 15 | )} 16 | {...props} 17 | /> 18 | ), 19 | (prevProps, nextProps) => prevProps.children === nextProps.children 20 | ); 21 | 22 | Response.displayName = "Response"; 23 | -------------------------------------------------------------------------------- /src/components/prose.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface ProseProps extends React.ComponentPropsWithoutRef<"article"> { 6 | html: string; 7 | } 8 | 9 | export function Prose({ children, html, className }: ProseProps) { 10 | return ( 11 |
17 | {html ?
: children} 18 |
19 | ); 20 | } -------------------------------------------------------------------------------- /src/server/routers/chat/tools/shared.ts: -------------------------------------------------------------------------------- 1 | import { UIMessageStreamWriter } from "ai" 2 | import { type PayloadTweet } from '@/hooks/use-tweets-v2' 3 | import { MyUIMessage } from "../chat-router" 4 | import { parseAttachments } from "../utils" 5 | 6 | export interface Context { 7 | writer: UIMessageStreamWriter 8 | ctx: { 9 | plan: 'free' | 'pro' 10 | tweets: PayloadTweet[] 11 | rawUserMessage: string 12 | messages: MyUIMessage[] 13 | attachments: Awaited> 14 | userId: string 15 | redisKeys: { 16 | thread: string 17 | style: string 18 | account: string 19 | websiteContent: string 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { getBaseUrl } from '@/constants/base-url' 2 | import type { MetadataRoute } from 'next' 3 | 4 | export default function sitemap(): MetadataRoute.Sitemap { 5 | const baseUrl = getBaseUrl() 6 | 7 | return [ 8 | { 9 | url: baseUrl, 10 | lastModified: new Date(), 11 | changeFrequency: 'daily', 12 | priority: 1, 13 | }, 14 | { 15 | url: `${baseUrl}/privacy`, 16 | lastModified: new Date(), 17 | changeFrequency: 'yearly', 18 | priority: 0.5, 19 | }, 20 | { 21 | url: `${baseUrl}/terms`, 22 | lastModified: new Date(), 23 | changeFrequency: 'yearly', 24 | priority: 0.5, 25 | }, 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/server/routers/utils/get-scheduled-tweet-count.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/db' 2 | import { tweets } from '@/db/schema' 3 | import { eq, gt, isNull, and } from 'drizzle-orm' 4 | 5 | export async function getScheduledTweetCount(userId: string): Promise { 6 | const currentTime = new Date().getTime() 7 | 8 | const futureScheduledTweets = await db.query.tweets.findMany({ 9 | where: and( 10 | eq(tweets.userId, userId), 11 | eq(tweets.isScheduled, true), 12 | eq(tweets.isError, false), 13 | gt(tweets.scheduledUnix, currentTime), 14 | isNull(tweets.isReplyTo), 15 | ), 16 | columns: { id: true }, 17 | }) 18 | 19 | return futureScheduledTweets.length 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/lexical-plugins/multiple-editor-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' 3 | import { useEditors } from '@/hooks/use-editors' 4 | 5 | export type MultipleEditorStorePluginProps = { 6 | id: string 7 | } 8 | 9 | export function MultipleEditorStorePlugin(props: MultipleEditorStorePluginProps) { 10 | const { id } = props 11 | const [editor] = useLexicalComposerContext() 12 | const editors = useEditors() 13 | useEffect(() => { 14 | editors.createEditor(id, editor) 15 | return () => editors.deleteEditor(id) 16 | }, [id, editor]) // eslint-disable-line react-hooks/exhaustive-deps 17 | return null 18 | } 19 | -------------------------------------------------------------------------------- /public/pattern/graphpaper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/api/realtime/route.ts: -------------------------------------------------------------------------------- 1 | import { handle } from '@upstash/realtime' 2 | import { realtime } from '@/lib/realtime' 3 | import { HTTPException } from 'hono/http-exception' 4 | import { auth } from '@/lib/auth' 5 | 6 | export const GET = handle({ 7 | realtime, 8 | middleware: async ({ request, channels }) => { 9 | const session = await auth.api.getSession({ headers: request.headers }) 10 | 11 | if (!session) { 12 | throw new HTTPException(403, { message: 'Log in to access this resource' }) 13 | } 14 | 15 | for (const channel of channels) { 16 | if (!channel.startsWith(session.user.id)) { 17 | return new Response('Forbidden', { status: 403 }) 18 | } 19 | } 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/ui/settings-section.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { LucideIcon } from "lucide-react" 3 | import { ReactNode } from "react" 4 | 5 | interface SettingsSectionProps { 6 | children: ReactNode 7 | className?: string 8 | icon?: LucideIcon 9 | title: string 10 | } 11 | 12 | export function SettingsSection({ children, className, icon: Icon, title }: SettingsSectionProps) { 13 | return ( 14 |
15 |
16 | {Icon && } 17 |

{title}

18 |
19 | {children} 20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /src/lib/merge-button-refs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This function merges multiple refs into a single ref callback. 3 | It ensures that the value is assigned to each ref, whether it's a function or a mutable ref object. 4 | This is useful when you need to merge external refs with an internal ref. 5 | */ 6 | 7 | export function mergeButtonRefs( 8 | refs: Array | React.LegacyRef> 9 | ): React.RefCallback { 10 | return (value) => { 11 | for (const ref of refs) { 12 | if (typeof ref === "function") { 13 | ref(value) 14 | } else if (ref != null) { 15 | ;(ref as React.MutableRefObject).current = value 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/studio/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardProviders } from '@/components/providers/dashboard-providers' 2 | import ClientLayout from '@/frontend/studio/layout' 3 | import { cookies } from 'next/headers' 4 | import { PropsWithChildren } from 'react' 5 | 6 | export default async function Layout({ children }: PropsWithChildren) { 7 | const cookieStore = await cookies() 8 | const sidebarWidth = cookieStore.get('sidebar:width') 9 | const sidebarState = cookieStore.get('sidebar:state') 10 | 11 | return ( 12 |
13 | 14 | 15 | {children} 16 | 17 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/studio/topic-monitor/empty-state.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from '@/components/ui/card' 2 | import DuolingoButton from '@/components/ui/duolingo-button' 3 | import { GhostIcon, XLogoIcon } from '@phosphor-icons/react' 4 | 5 | interface EmptyStateProps { 6 | title: string 7 | description: string 8 | } 9 | 10 | export const EmptyState = ({ title, description }: EmptyStateProps) => { 11 | return ( 12 | 13 |
14 | 15 |

{title}

16 |

{description}

17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/tweet-editor/tweet-editor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { HTMLAttributes } from 'react' 5 | import Tweet from './tweet' 6 | 7 | interface TweetEditorProps extends HTMLAttributes { 8 | id?: string | undefined 9 | initialContent?: string 10 | editMode?: boolean 11 | } 12 | 13 | export default function TweetEditor({ 14 | id, 15 | initialContent, 16 | className, 17 | editMode = false, 18 | ...rest 19 | }: TweetEditorProps) { 20 | return ( 21 |
22 |
23 | 24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/realtime.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod/v4' 2 | import { Realtime, InferRealtimeEvents } from '@upstash/realtime' 3 | import { redis } from './redis' 4 | 5 | const schema = { 6 | index_tweets: { 7 | status: z.enum(['started', 'resolved']), 8 | }, 9 | index_memories: { 10 | status: z.enum(['started', 'success', 'error']), 11 | }, 12 | tweet: { 13 | status: z.object({ 14 | databaseTweetId: z.string(), 15 | status: z.enum(['started', 'waiting', 'success', 'error']), 16 | twitterTweetId: z.string().optional(), 17 | timestamp: z.number().optional(), 18 | }), 19 | }, 20 | } 21 | 22 | export const realtime = new Realtime({ 23 | schema, 24 | redis, 25 | verbose: true, 26 | }) 27 | 28 | export type RealtimeEvents = InferRealtimeEvents 29 | -------------------------------------------------------------------------------- /jobs/transcription/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda", 3 | "version": "0.1.0", 4 | "bin": { 5 | "lambda": "bin/lambda.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/aws-lambda": "^8.10.152", 15 | "@types/jest": "^29.5.12", 16 | "@types/node": "^20.14.2", 17 | "aws-cdk": "2.146.0", 18 | "jest": "^29.7.0", 19 | "ts-jest": "^29.1.4", 20 | "ts-node": "^10.9.2", 21 | "typescript": "~5.4.5" 22 | }, 23 | "dependencies": { 24 | "aws-cdk-lib": "^2.146.0", 25 | "aws-sdk": "^2.1692.0", 26 | "constructs": "^10.0.0", 27 | "dotenv": "^17.2.0", 28 | "openai": "^5.10.2", 29 | "source-map-support": "^0.5.21" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/senja-widget.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { useEffect } from 'react' 5 | 6 | export const SenjaWidget = ({ className }: { className?: string }) => { 7 | useEffect(() => { 8 | const script = document.createElement('script') 9 | script.src = 10 | 'https://widget.senja.io/widget/80f6bf95-1c4d-42b3-b4a7-0b0c8c0d7a38/platform.js' 11 | script.async = true 12 | document.body.appendChild(script) 13 | 14 | return () => { 15 | document.body.removeChild(script) 16 | } 17 | }, []) 18 | 19 | return ( 20 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |