├── supabase ├── migrations │ ├── 20250607083909_remote_schema.sql │ ├── 20250607151037_cleanup_duplicate_policies.sql │ ├── 20250607212538_ensure_chat_realtime.sql │ ├── 20250607205729_fix_chat_rls_policies.sql │ └── 20250607180001_fix_admin_function_security.sql └── .gitignore ├── public ├── favicon.ico ├── images │ ├── lady transparent.png │ ├── sun-transparent-2.png │ ├── hands-transparent-4.png │ ├── arrow-left-2.svg │ ├── arrow-right-1.svg │ ├── arrow-right-3.svg │ └── placeholder-tile.svg └── robots.txt ├── postcss.config.cjs ├── src ├── styles │ └── google-fonts.ts ├── app │ ├── contests │ │ └── layout.tsx │ ├── admin │ │ ├── page.tsx │ │ ├── lookup │ │ │ ├── page.tsx │ │ │ └── layout.tsx │ │ └── layout.tsx │ ├── u │ │ └── [user] │ │ │ ├── template.tsx │ │ │ ├── loading.tsx │ │ │ ├── about │ │ │ └── page.tsx │ │ │ └── bookmarks │ │ │ └── page.tsx │ ├── settings │ │ ├── profile │ │ │ └── page.tsx │ │ ├── app │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── b │ │ │ └── [blog] │ │ │ └── page.tsx │ ├── send │ │ └── page.tsx │ ├── wrap │ │ └── page.tsx │ ├── home │ │ └── loading.tsx │ ├── error.tsx │ ├── chat │ │ └── page.tsx │ ├── bookmarks │ │ └── page.tsx │ ├── sitemap.ts │ ├── notifications │ │ └── page.tsx │ ├── b │ │ └── [blog] │ │ │ ├── loading.tsx │ │ │ └── [slug] │ │ │ └── page.tsx │ ├── api │ │ ├── newsletter │ │ │ ├── utils.ts │ │ │ └── [blog] │ │ │ │ └── subscribers │ │ │ │ └── all │ │ │ │ └── route.ts │ │ ├── contests │ │ │ └── route.ts │ │ └── email │ │ │ └── route.ts │ ├── page.tsx │ ├── search │ │ └── loading.tsx │ └── w │ │ └── [id] │ │ └── page.tsx ├── lib │ ├── lens │ │ ├── storage-client.ts │ │ ├── storage.ts │ │ └── client.ts │ ├── settings │ │ ├── user-settings.ts │ │ ├── get-user-email.ts │ │ ├── get-user-settings.ts │ │ ├── events.ts │ │ └── get-blogs-by-owner.ts │ ├── db │ │ ├── client.ts │ │ ├── middleware.ts │ │ ├── service.ts │ │ └── server.ts │ ├── seo │ │ ├── constants.ts │ │ └── canonical.ts │ ├── utils │ │ ├── is-evm-address.ts │ │ ├── find-blog-by-id.ts │ │ ├── image-optimization.ts │ │ ├── get-post-url.ts │ │ ├── resolve-url.ts │ │ └── ban-filter.ts │ ├── global-window.ts │ ├── auth │ │ ├── clear-cookies.ts │ │ ├── get-token-claims.ts │ │ ├── verify-token.ts │ │ ├── is-guest-user.ts │ │ ├── get-app-token.ts │ │ ├── get-session.ts │ │ ├── validate-token.ts │ │ ├── app-token.ts │ │ ├── verify-auth-request.ts │ │ ├── is-admin.ts │ │ ├── sign-app-token.ts │ │ ├── sign-guest-token.ts │ │ ├── admin-middleware.ts │ │ └── get-user-profile.ts │ ├── get-base-url.ts │ ├── publish │ │ ├── get-feed-address.ts │ │ ├── delete-cloud-draft.ts │ │ ├── create-post-record.ts │ │ └── get-post-content.ts │ ├── extract-subtitle.ts │ ├── load-embed-js.tsx │ ├── scripts │ │ └── get-articles-gql.js │ ├── plate │ │ └── default-content.ts │ ├── slug │ │ └── get-post-by-slug.ts │ └── get-arweave-content.ts ├── components │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── search-highlight-leaf.tsx │ │ ├── code-line-element.tsx │ │ ├── skeleton.tsx │ │ ├── static │ │ │ ├── code-line-element-static.tsx │ │ │ ├── table-row-element-static.tsx │ │ │ ├── column-group-element-static.tsx │ │ │ ├── code-syntax-leaf-static.tsx │ │ │ ├── blockquote-element-static.tsx │ │ │ ├── code-leaf-static.tsx │ │ │ ├── comment-leaf-static.tsx │ │ │ ├── link-element-static.tsx │ │ │ ├── paragraph-element-static.tsx │ │ │ ├── column-element-static.tsx │ │ │ ├── hr-element-static.tsx │ │ │ ├── indent-todo-marker-static.tsx │ │ │ ├── media-audio-element-static.tsx │ │ │ ├── code-block-element-static.tsx │ │ │ ├── toggle-element-static.tsx │ │ │ ├── heading-element-static.tsx │ │ │ ├── table-element-static.tsx │ │ │ ├── checkbox-static.tsx │ │ │ ├── callout-element-static.tsx │ │ │ ├── media-video-element-static.tsx │ │ │ ├── image-element-static.tsx │ │ │ ├── media-file-element-static.tsx │ │ │ └── equation-element-static.tsx │ │ ├── fixed-toolbar.tsx │ │ ├── list-item.tsx │ │ ├── collapsible.tsx │ │ ├── highlight-leaf.tsx │ │ ├── blockquote-element.tsx │ │ ├── code-syntax-leaf.tsx │ │ ├── column-group-element.tsx │ │ ├── avatar.tsx │ │ ├── code-leaf.tsx │ │ ├── indent-toolbar-button.tsx │ │ ├── table-row-element.tsx │ │ ├── outdent-toolbar-button.tsx │ │ ├── ai-leaf.tsx │ │ ├── kbd-leaf.tsx │ │ ├── paragraph-element.tsx │ │ ├── link-toolbar-button.tsx │ │ ├── ai-toolbar-button.tsx │ │ ├── toggle-toolbar-button.tsx │ │ ├── indent-todo-toolbar-button.tsx │ │ ├── separator.tsx │ │ ├── media-toolbar-button.tsx │ │ ├── inline-equation-toolbar-button.tsx │ │ ├── emoji-dropdown-menu.tsx │ │ ├── emoji-toolbar-dropdown.tsx │ │ ├── label.tsx │ │ ├── toaster.tsx │ │ ├── link-element.tsx │ │ ├── list-element.tsx │ │ ├── mark-toolbar-button.tsx │ │ ├── column-element.tsx │ │ ├── progress.tsx │ │ ├── ghost-text.tsx │ │ ├── animated-chevron.tsx │ │ ├── color-input.tsx │ │ ├── list-toolbar-button.tsx │ │ ├── indent-list-toolbar-button.tsx │ │ ├── indent-todo-marker.tsx │ │ ├── hr-element.tsx │ │ ├── sonner.tsx │ │ ├── emoji-picker-search-bar.tsx │ │ ├── metadata-display.tsx │ │ ├── todo-list-element.tsx │ │ ├── block-selection.tsx │ │ ├── toggle-element.tsx │ │ ├── slider.tsx │ │ ├── input.tsx │ │ ├── title-element.tsx │ │ ├── subtitle-element.tsx │ │ ├── emoji-picker-search-and-clear.tsx │ │ ├── badge.tsx │ │ ├── caption.tsx │ │ └── hover-card.tsx │ ├── user │ │ ├── user-socials.tsx │ │ ├── user-handle.tsx │ │ ├── user-bio.tsx │ │ ├── user-location.tsx │ │ ├── user-lazy-username.tsx │ │ ├── user-name.tsx │ │ ├── user-following.tsx │ │ ├── user-cover.tsx │ │ ├── user-site.tsx │ │ └── user-navigation.tsx │ ├── misc │ │ ├── loading-spinner.tsx │ │ ├── global-modals.tsx │ │ ├── error-page.tsx │ │ └── truncated-text.tsx │ ├── seo │ │ └── structured-data.tsx │ ├── navigation │ │ ├── article-layout.tsx │ │ ├── search-layout.tsx │ │ ├── gradient-blur.tsx │ │ ├── feed-navigation.tsx │ │ └── page-transition.tsx │ ├── icons │ │ └── bell.tsx │ ├── post │ │ ├── post-skeleton-boundary.tsx │ │ └── post-deleted-view.tsx │ ├── admin │ │ └── admin-auth-check.tsx │ ├── theme │ │ └── theme-toggle.tsx │ ├── auth │ │ └── auth-wallet-button.tsx │ ├── editor │ │ ├── transforms │ │ │ └── insert-iframe.ts │ │ ├── addons │ │ │ └── editor-read-time.tsx │ │ └── plugins │ │ │ ├── title-plugin.ts │ │ │ ├── iframe-plugin.ts │ │ │ └── blockquote-normalize-plugin.ts │ ├── draft │ │ └── draft.ts │ ├── chat │ │ └── chat-user-info.tsx │ ├── settings │ │ └── settings-newsletter.tsx │ └── feed │ │ └── feed-view-toggle.tsx ├── hooks │ ├── use-reconnect-wallet.ts │ ├── use-sync-value-effect.ts │ ├── use-mounted.ts │ ├── use-unmount.ts │ ├── use-sidebar.tsx │ ├── use-image-preload.ts │ ├── use-initial-state.ts │ ├── use-object-version.ts │ ├── use-isomorphic-layout-effect.ts │ ├── use-origin.tsx │ ├── use-scrolled.ts │ ├── use-debounce.ts │ ├── use-safe-memo.ts │ ├── use-is-touch.ts │ ├── use-is-touch-device.ts │ ├── use-enter-submit.ts │ ├── use-lock-body.ts │ ├── use-mobile.tsx │ ├── use-filter-skills.ts │ ├── use-debounce-pending-click.ts │ ├── use-viewport.ts │ ├── use-get-window-height.ts │ ├── use-storage.ts │ ├── use-effect-after-first.ts │ ├── use-feed-view-mode.ts │ ├── use-lens-clients.ts │ ├── use-lock-scroll.ts │ ├── use-copy-to-clipboard.ts │ ├── use-on-screen.ts │ ├── use-pipe-ref.ts │ ├── use-publish-draft.ts │ ├── use-admin-status.ts │ ├── use-at-bottom.ts │ ├── use-document-storage.ts │ ├── use-click-outside.ts │ ├── use-intersection-observer.ts │ ├── use-get-data.ts │ ├── use-local-storage.ts │ ├── use-media-query.ts │ └── use-cookie-storage.ts ├── contexts │ ├── action-bar-context.tsx │ └── feed-context.tsx └── stores │ └── ui-store.ts ├── .github └── ISSUE_TEMPLATE │ ├── feature.md │ └── bug.md ├── components.json ├── .gitignore ├── tsconfig.json ├── emails └── newsletter-email-test.tsx └── next.config.js /supabase/migrations/20250607083909_remote_schema.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fountain-ink/app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/lady transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fountain-ink/app/HEAD/public/images/lady transparent.png -------------------------------------------------------------------------------- /public/images/sun-transparent-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fountain-ink/app/HEAD/public/images/sun-transparent-2.png -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | 5 | # dotenvx 6 | .env.keys 7 | .env.local 8 | .env.*.local 9 | -------------------------------------------------------------------------------- /public/images/hands-transparent-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fountain-ink/app/HEAD/public/images/hands-transparent-4.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /src/styles/google-fonts.ts: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | 3 | export const inter = Inter({ subsets: ["latin"] }); 4 | -------------------------------------------------------------------------------- /src/app/contests/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function ContestsLayout({ children }: { children: React.ReactNode }) { 2 | return children; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default function AdminPage() { 4 | redirect("/admin/feedback"); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/lens/storage-client.ts: -------------------------------------------------------------------------------- 1 | import { StorageClient } from "@lens-chain/storage-client"; 2 | 3 | export const storageClient = StorageClient.create(); 4 | -------------------------------------------------------------------------------- /src/lib/settings/user-settings.ts: -------------------------------------------------------------------------------- 1 | export interface UserSettings { 2 | app?: { 3 | isSmoothScrolling?: boolean; 4 | isBlurEnabled?: boolean; 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root; 6 | 7 | export { AspectRatio }; 8 | -------------------------------------------------------------------------------- /src/components/ui/search-highlight-leaf.tsx: -------------------------------------------------------------------------------- 1 | import { withCn } from "@udecode/cn"; 2 | import { PlateLeaf } from "@udecode/plate/react"; 3 | 4 | export const SearchHighlightLeaf = withCn(PlateLeaf, "bg-yellow-100"); 5 | -------------------------------------------------------------------------------- /src/components/user/user-socials.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Account } from "@lens-protocol/client"; 4 | 5 | export const UserSocials = ({ profile }: { profile?: Account }) => { 6 | return
; 7 | }; 8 | -------------------------------------------------------------------------------- /src/hooks/use-reconnect-wallet.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useModal } from "connectkit"; 3 | 4 | export const useReconnectWallet = () => { 5 | const { setOpen } = useModal(); 6 | return () => setOpen(true); 7 | }; 8 | -------------------------------------------------------------------------------- /src/hooks/use-sync-value-effect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const useSyncValueEffect = (value: boolean, setter: (value: boolean) => void) => { 4 | useEffect(() => { 5 | setter(value); 6 | }, [value, setter]); 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/db/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from "@supabase/ssr"; 2 | import { env } from "@/env"; 3 | 4 | export function createClient() { 5 | return createBrowserClient(env.NEXT_PUBLIC_SUPABASE_URL, env.NEXT_PUBLIC_SUPABASE_ANON_KEY); 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/seo/constants.ts: -------------------------------------------------------------------------------- 1 | export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://fountain.ink"; 2 | export const SITE_NAME = "Fountain"; 3 | export const SITE_DESCRIPTION = "An open blogging platform"; 4 | export const DEFAULT_AUTHOR = "Fountain"; 5 | -------------------------------------------------------------------------------- /src/app/u/[user]/template.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AnimatePresence } from "motion/react"; 4 | 5 | export default function Template({ children }: { children: React.ReactNode }) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/use-mounted.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function useMounted() { 4 | const [mounted, setMounted] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | setMounted(true); 8 | }, []); 9 | 10 | return mounted; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ui/code-line-element.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { withRef } from "@udecode/cn"; 4 | import { PlateElement } from "@udecode/plate/react"; 5 | 6 | export const CodeLineElement = withRef((props, ref) => ); 7 | -------------------------------------------------------------------------------- /src/app/u/[user]/loading.tsx: -------------------------------------------------------------------------------- 1 | import PostSkeleton from "@/components/post/post-skeleton"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return
; 5 | } 6 | 7 | export { Skeleton }; 8 | -------------------------------------------------------------------------------- /src/hooks/use-unmount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export function useUnmount(func: () => void) { 4 | const funcRef = useRef(func); 5 | 6 | funcRef.current = func; 7 | 8 | useEffect( 9 | () => () => { 10 | funcRef.current(); 11 | }, 12 | [], 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/utils/is-evm-address.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if a string is a valid EVM address. 3 | * @param address The string to check 4 | * @returns true if the string is a valid EVM address, false otherwise 5 | */ 6 | export function isEvmAddress(address: string): boolean { 7 | return /^0x[a-fA-F0-9]{40}$/.test(address); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ui/static/code-line-element-static.tsx: -------------------------------------------------------------------------------- 1 | import type { SlateElementProps } from "@udecode/plate"; 2 | 3 | import { SlateElement } from "@udecode/plate"; 4 | 5 | export const CodeLineElementStatic = ({ children, ...props }: SlateElementProps) => { 6 | return {children}; 7 | }; 8 | -------------------------------------------------------------------------------- /src/hooks/use-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | export function useSidebar() { 6 | const [state, setState] = React.useState<"closed" | "open">("open"); 7 | 8 | return { 9 | open: state === "open", 10 | onOpenChange: (open: boolean) => setState(open ? "open" : "closed"), 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/use-image-preload.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | 3 | export function useImagePreload() { 4 | const preloadImage = useCallback((src: string) => { 5 | if (typeof window === "undefined" || !src) return; 6 | 7 | const img = new Image(); 8 | img.src = src; 9 | }, []); 10 | 11 | return preloadImage; 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/use-initial-state.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const useInitialState = (value: T) => { 4 | const [initialValue] = useState(value); 5 | 6 | return initialValue; 7 | }; 8 | 9 | export const useUpdatedState = (value: T) => { 10 | const initial = useInitialState(value); 11 | 12 | return initial !== value; 13 | }; 14 | -------------------------------------------------------------------------------- /src/hooks/use-object-version.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | export const useObjectVersion = (obj: unknown): number => { 4 | const versionRef = useRef(0); 5 | const prevObjRef = useRef(obj); 6 | 7 | if (prevObjRef.current !== obj) { 8 | versionRef.current++; 9 | prevObjRef.current = obj; 10 | } 11 | 12 | return versionRef.current; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/misc/loading-spinner.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderCircleIcon } from "lucide-react"; 2 | 3 | export const LoadingSpinner = ({ size = 22, className }: { size?: number; className?: string }) => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/global-window.ts: -------------------------------------------------------------------------------- 1 | import { maybe, never } from "@lens-protocol/shared-kernel"; 2 | 3 | const safeGlobal = (maybe(() => globalThis) ?? 4 | maybe(() => window) ?? 5 | maybe(() => self) ?? 6 | maybe(() => global) ?? 7 | never("Cannot resolve a global object.")) as typeof globalThis & Window; 8 | 9 | export const window = maybe(() => safeGlobal.window) ?? null; 10 | -------------------------------------------------------------------------------- /src/lib/utils/find-blog-by-id.ts: -------------------------------------------------------------------------------- 1 | import { isEvmAddress } from "@/lib/utils/is-evm-address"; 2 | 3 | export async function findBlogByIdentifier(db: any, identifier: string) { 4 | if (isEvmAddress(identifier)) { 5 | return await db.from("blogs").select("*").eq("address", identifier).single(); 6 | } 7 | return await db.from("blogs").select("*").eq("handle", identifier).single(); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ui/fixed-toolbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { withCn } from "@udecode/cn"; 4 | 5 | import { Toolbar } from "./toolbar"; 6 | 7 | export const FixedToolbar = withCn( 8 | Toolbar, 9 | "supports-backdrop-blur:bg-background/60 sticky left-0 top-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur", 10 | ); 11 | -------------------------------------------------------------------------------- /src/app/admin/lookup/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useEffect } from "react"; 5 | 6 | export default function LookupPage() { 7 | const router = useRouter(); 8 | 9 | useEffect(() => { 10 | router.replace("/admin/feedback"); 11 | }, [router]); 12 | 13 | return
Redirecting...
; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/user/user-handle.tsx: -------------------------------------------------------------------------------- 1 | import { Account } from "@lens-protocol/client"; 2 | 3 | export const UserUsername = ({ account, className }: { account?: Account | null; className?: string }) => { 4 | const username = account?.username?.localName; 5 | if (!username) { 6 | return null; 7 | } 8 | 9 | return
@{username}
; 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/seo/structured-data.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | 3 | interface StructuredDataProps { 4 | data: any; 5 | } 6 | 7 | export function StructuredData({ data }: StructuredDataProps) { 8 | return ( 9 |