├── .dockerignore ├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── app ├── components │ ├── FarcasterIcon.tsx │ ├── cast-curation-form.tsx │ ├── color-picker.tsx │ ├── container.tsx │ ├── curation-form.tsx │ ├── icons │ │ └── farcaster.tsx │ ├── moxie-picker.tsx │ ├── rule-display-form.tsx │ ├── rule-editor.tsx │ ├── sub-nav.tsx │ ├── ui │ │ ├── accordion.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── fields.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx │ └── user-picker.tsx ├── entry.client.tsx ├── entry.server.tsx ├── lib │ ├── abis.ts │ ├── airstack.server.ts │ ├── alchemy.server.ts │ ├── auth.server.ts │ ├── authkey.server.ts │ ├── automod.server.ts │ ├── bullish.server.ts │ ├── cache.server.ts │ ├── cast-actions.server.ts │ ├── cast-mod.server.ts │ ├── castsense.server.ts │ ├── db.server.ts │ ├── farcaster-strategy.ts │ ├── god-strategy.ts │ ├── http.server.ts │ ├── languages.ts │ ├── neynar.server.ts │ ├── notifications.server.ts │ ├── otp-strategy.ts │ ├── permissions.server.ts │ ├── simplehash.server.ts │ ├── singleton.server.ts │ ├── stats.server.ts │ ├── subscription.server.ts │ ├── types.ts │ ├── utils.server.ts │ ├── utils.tsx │ ├── validations.server.ts │ ├── viem.server.ts │ └── warpcast.server.ts ├── root.css ├── root.tsx ├── routes │ ├── _index.tsx │ ├── api.channels.$id.toggleEnable.tsx │ ├── api.channels.$id.tsx │ ├── api.channels.tsx │ ├── api.images.tsx │ ├── api.partners.channels.$id.activity.export.tsx │ ├── api.partners.channels.$id.activity.tsx │ ├── api.partners.channels.$id.roles.$roleId.tsx │ ├── api.partners.channels.$id.roles.tsx │ ├── api.searchFarcasterUser.tsx │ ├── api.searchMoxieMemberTokens.tsx │ ├── api.signer.tsx │ ├── api.transaction.$channel.tsx │ ├── api.warpcast.channels.$id.ts │ ├── api.webhooks.alchemy.tsx │ ├── api.webhooks.dev.tsx │ ├── api.webhooks.neynar.tsx │ ├── auth.farcaster.tsx │ ├── auth.god.tsx │ ├── channels.$channel.join.tsx │ ├── channels.$channel.paid.tsx │ ├── channels.$id.tsx │ ├── channels._index.tsx │ ├── channels.tsx │ ├── disclosure.tsx │ ├── login.tsx │ ├── maintenance.tsx │ ├── pricing.tsx │ ├── privacy.tsx │ ├── signer.tsx │ ├── tos.tsx │ ├── ~._index.tsx │ ├── ~.account.tsx │ ├── ~.admin.tsx │ ├── ~.channels.$id._index.tsx │ ├── ~.channels.$id.activity._index.tsx │ ├── ~.channels.$id.activity.casts.tsx │ ├── ~.channels.$id.bans.tsx │ ├── ~.channels.$id.casts.tsx │ ├── ~.channels.$id.collaborators.tsx │ ├── ~.channels.$id.edit.tsx │ ├── ~.channels.$id.roles.tsx │ ├── ~.channels.$id.roles_.$role._index.tsx │ ├── ~.channels.$id.roles_.$role.tsx │ ├── ~.channels.$id.roles_.$role.users.tsx │ ├── ~.channels.$id.settings.tsx │ ├── ~.channels.$id.tools.tsx │ ├── ~.channels.$id.tsx │ ├── ~.channels.new.1.tsx │ ├── ~.channels.new.2.tsx │ ├── ~.channels.new.3.tsx │ ├── ~.channels.new.5.tsx │ ├── ~.channels.new.tsx │ ├── ~.channels.tsx │ ├── ~.logout.tsx │ └── ~.tsx └── rules │ ├── airstack.ts │ ├── bot-or-not.ts │ ├── cast-content.ts │ ├── erc-tokens.ts │ ├── fantoken.ts │ ├── hypersub.ts │ ├── icebreaker.ts │ ├── membership-fee.ts │ ├── membership.ts │ ├── openrank.ts │ ├── paragrah.ts │ ├── powerbadge.ts │ ├── regular.ts │ ├── rules.type.ts │ ├── user-fid.ts │ ├── user-follow.ts │ ├── user-profile.ts │ └── webhook.ts ├── bullboard ├── index.ts └── removejob.ts ├── components.json ├── package.json ├── pnpm-lock.yaml ├── prisma ├── migrations │ ├── 20240701053649_init │ │ └── migration.sql │ ├── 20240708015729_add_feedtype │ │ └── migration.sql │ ├── 20240725061409_add_index_and_api_key │ │ └── migration.sql │ ├── 20240816131710_add_plan_wallet_address │ │ └── migration.sql │ ├── 20240903055328_add_prop_delay_check │ │ └── migration.sql │ ├── 20240903063448_wat │ │ └── migration.sql │ ├── 20240919024329_drop_rule_sets │ │ └── migration.sql │ ├── 20240925210434_add_rule_in_log │ │ └── migration.sql │ ├── 20241007044912_update_cast_log │ │ └── migration.sql │ ├── 20241008055905_add_cast_rule_set_and_banlist │ │ └── migration.sql │ ├── 20241008060703_remove_columns │ │ └── migration.sql │ ├── 20241010043138_add_slow_mode_hours │ │ └── migration.sql │ ├── 20241011164744_add_frames_field │ │ └── migration.sql │ ├── 20241012162145_add_channel_order │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seed.ts ├── public ├── 1up.wav ├── about.txt ├── actions │ ├── ban.png │ ├── bypass.png │ ├── cooldown24.png │ ├── curate.png │ ├── downvote.png │ ├── hideQuietly.png │ ├── install-scoped.png │ └── no-actions-available.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon-120x120-precomposed.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-precompose.png ├── apple-touch-icon.png ├── automod-cast-action-bg.png ├── drukwide.ttf ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts │ ├── inter-medium.ttf │ └── kode-mono-bold.ttf ├── glass.png ├── icons │ ├── airstack.png │ ├── automod.png │ ├── botornot.png │ ├── fabric.svg │ ├── hypersub.png │ ├── icebreaker.png │ ├── logo.png │ ├── logo.svg │ ├── modbot.png │ ├── moxie.png │ ├── neynar.png │ ├── openrank.png │ └── paragraph2.png ├── logo.png ├── manifest.json ├── robots.txt ├── screenshots │ └── screen.png ├── site.webmanifest └── videos │ └── automod-demo-complete.mp4 ├── remix.config.js ├── remix.env.d.ts ├── scripts └── dbReset.sh ├── start.sh ├── tailwind.config.js ├── tests ├── fixtures.ts ├── setup.ts └── unit │ ├── validations.test.ts │ └── webhook.test.ts ├── tsconfig.json ├── tsconfig.seed.json └── vitest.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # public facing url 2 | PROD_URL=https://automod.sh 3 | 4 | # For local dev only, you may set this to ngrok 5 | # and other tunnels to test things 6 | DEV_URL=http://localhost:3000 7 | 8 | # Secret for all auth tokens, dont fuck this up 9 | SESSION_SECRET= 10 | 11 | # Postgres database url 12 | DATABASE_URL=postgresql://user:password@host:port/database 13 | 14 | # Redis used for all queuing 15 | REDIS_URL=redis://localhost:6379 16 | 17 | # Available from infura.io 18 | INFURA_PROJECT_ID= 19 | 20 | # Available from alchemy.com 21 | ALCHEMY_API_KEY= 22 | 23 | # Available via Coinbase Developer Portal, provision 24 | # multiple for rate limiting fallbacks. 25 | BASE_RPC_URL= 26 | BASE_RPC_URL2= 27 | BASE_RPC_URL3= 28 | BASE_RPC_URL4= 29 | 30 | # Used for select token apis like 1155s, acquire from 31 | # https://simplehash.com/ 32 | SIMPLE_HASH_API_KEY= 33 | 34 | # Airstack API key, acquire from https://airstack.xyz/ 35 | AIRSTACK_API_KEY= 36 | 37 | # Graph API key used for Moxie Fan Token lookups, 38 | # acquire from https://thegraph.com/ 39 | GRAPH_API_KEY= 40 | 41 | ## Automod factory is used to monitor Farcaster protocol 42 | # health between neynar <-> warpcast. 43 | # Neynar managed signer uuid, create via api or /signer 44 | AUTOMOD_FACTORY_UUID= 45 | # Long lived warpcast token, create via https://github.com/davidfurlong/farcaster-auth-tokens 46 | AUTOMOD_FACTORY_WARPCAST_TOKEN= 47 | 48 | # Visit dev.neynar.com console 49 | NEYNAR_CLIENT_ID= 50 | NEYNAR_API_KEY= 51 | 52 | # Webhook resource id to track all webhook subscriptions, 53 | # example: https://dev.neynar.com/webhook/01J4ND4V9A78MMGZ07XKFHNSRQ 54 | NEYNAR_WEBHOOK_ID= 55 | NEYNAR_WEBHOOK_SECRET= 56 | 57 | # Default neynar managed signer uuid to issue likes and other 58 | # protocol actions from, e.g. @automod. provision from neynar 59 | # dashboard or through API. 60 | NEYNAR_SIGNER_UUID= 61 | 62 | # Account notifications delivered from @automod like usage warnings, etc. 63 | WARPCAST_DM_KEY= 64 | 65 | # Powers OpenRank rules 66 | OPENRANK_API_KEY= 67 | 68 | # Enable queues 69 | ENABLE_QUEUES=false 70 | 71 | # Hypersub contract addresses for billing 72 | # Legacy deprecated hypersub 73 | PRIME_CONTRACT_ADDRESS="0x789fa3cce06a59d5c7e138e94f32793a5f30adaa" 74 | 75 | # New unified hypersub v2 contract w/tiers 76 | HYPERSUBV2_CONTRACT_ADDRESS="0xc8afc8c132c848534ceff21e80f518cc81628b24" 77 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is intended to be a basic starting point for linting in your app. 3 | * It relies on recommended configs out of the box for simplicity, but you can 4 | * and should modify this configuration to best suit your team's needs. 5 | */ 6 | 7 | /** @type {import('eslint').Linter.Config} */ 8 | module.exports = { 9 | root: true, 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | env: { 18 | browser: true, 19 | commonjs: true, 20 | es6: true, 21 | }, 22 | 23 | // Base config 24 | extends: ["eslint:recommended"], 25 | overrides: [ 26 | // React 27 | { 28 | files: ["**/*.{js,jsx,ts,tsx}"], 29 | plugins: ["react"], 30 | extends: [ 31 | "plugin:react/recommended", 32 | "plugin:react/jsx-runtime", 33 | "plugin:react-hooks/recommended", 34 | ], 35 | rules: { 36 | "react/no-unescaped-entities": "off", 37 | }, 38 | settings: { 39 | react: { 40 | version: "detect", 41 | }, 42 | formComponents: ["Form"], 43 | linkComponents: [ 44 | { name: "Link", linkAttribute: "to" }, 45 | { name: "NavLink", linkAttribute: "to" }, 46 | ], 47 | "import/resolver": { 48 | typescript: {}, 49 | }, 50 | }, 51 | }, 52 | 53 | // Typescript 54 | { 55 | files: ["**/*.{ts,tsx}"], 56 | plugins: ["@typescript-eslint", "import"], 57 | parser: "@typescript-eslint/parser", 58 | settings: { 59 | "import/internal-regex": "^~/", 60 | "import/resolver": { 61 | node: { 62 | extensions: [".ts", ".tsx"], 63 | }, 64 | typescript: { 65 | alwaysTryTypes: true, 66 | }, 67 | }, 68 | }, 69 | rules: { 70 | "@typescript-eslint/no-unused-vars": "off", 71 | }, 72 | extends: [ 73 | "plugin:@typescript-eslint/recommended", 74 | "plugin:import/recommended", 75 | "plugin:import/typescript", 76 | ], 77 | }, 78 | 79 | // Node 80 | { 81 | files: [".eslintrc.js"], 82 | env: { 83 | node: true, 84 | }, 85 | }, 86 | ], 87 | }; 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | *.db 4 | /prisma/*.db 5 | /prisma/*.db* 6 | /.cache 7 | /build 8 | /public/build 9 | .env 10 | .DS_Store 11 | docs.txt 12 | # Sentry Config File 13 | .sentryclirc 14 | scripts/*.json 15 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run typecheck -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.git": true, 4 | "**/node_modules": true, 5 | "package-lock.json": true, 6 | "**/.cache": true 7 | }, 8 | "typescript.preferences.importModuleSpecifier": "non-relative" 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1 2 | 3 | # Adjust NODE_VERSION as desired 4 | ARG NODE_VERSION=20.3.0 5 | FROM node:${NODE_VERSION}-slim as base 6 | 7 | LABEL fly_launch_runtime="Remix" 8 | 9 | # Remix app lives here 10 | WORKDIR /app 11 | 12 | # Set production environment 13 | ENV NODE_ENV="production" 14 | ENV PORT="3000" 15 | 16 | # Install pnpm 17 | ARG PNPM_VERSION=9.6.0 18 | RUN apt-get update -qq && \ 19 | apt-get install --no-install-recommends -y openssl sqlite3 && \ 20 | npm install -g pnpm@$PNPM_VERSION 21 | 22 | # Throw-away build stage to reduce size of final image 23 | FROM base as build 24 | 25 | ARG SENTRY_AUTH_TOKEN 26 | ENV SENTRY_AUTH_TOKEN $SENTRY_AUTH_TOKEN 27 | 28 | # Install packages needed to build node modules 29 | RUN apt-get update -qq && \ 30 | apt-get install --no-install-recommends -y build-essential sqlite3 node-gyp pkg-config python-is-python3 python3-pip 31 | 32 | ENV PYTHON=/usr/bin/python3 33 | 34 | # Install node modules 35 | COPY --link package.json pnpm-lock.yaml ./ 36 | RUN pnpm install --frozen-lockfile --prod=false 37 | 38 | # Copy application code 39 | COPY --link . . 40 | 41 | # Build application 42 | RUN pnpm run build 43 | 44 | # Remove development dependencies 45 | # RUN pnpm prune --prod 46 | 47 | 48 | # Final stage for app image 49 | FROM base 50 | 51 | # Istall openssl n shit 52 | 53 | 54 | # Copy built application 55 | COPY --from=build /app /app 56 | 57 | # Start the server by default, this can be overwritten at runtime 58 | EXPOSE 3000 59 | ENTRYPOINT [ "./start.sh" ] 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | ModBot is a Farcaster channel moderation service. It allows channel hosts to configure rules to automatically invite members and manage their channel in teams. The code is forked from [Automod](https://github.com/jtgi/automod). Huge thanks to [@jtgi](https://warpcast.com/jtgi/) for his great work. 4 | 5 | ### Tech Stack 6 | 7 | - **API Framework:** [Remix](https://remix.run) 8 | - **Database:** PostgreSQL / [Prisma](https://www.prisma.io) 9 | - **Queues:** [BullMQ](https://docs.bullmq.io/) with Redis 10 | - **Hosting:** Anywhere that supports docker, node, or remix. 11 | 12 | ### How to add a new Rule 13 | 14 | - Add a new rule name in [validations.server.ts](/app/rules/rules.type.ts). 15 | - Create your rule file and implement RuleDefinition/RulesFunction in [app/rules](/app/rules).There are many examples to reference. 16 | - Include `RuleName` and `RuleDefinition` to [validations.server.ts](/app/lib/validations.server.ts). 17 | 18 | ## Getting Started 19 | 20 | ### Local Development 21 | 22 | ```sh 23 | git clone https://github.com/kale5195/ModBot.git 24 | cd ModBot 25 | pnpm install 26 | cp .env.example .env 27 | # Update .env with your configuration, see [.env.example](/.env.example) for instructions 28 | pnpm run dev 29 | ``` 30 | 31 | ## Costs 32 | 33 | At time of writing: 34 | 35 | - Powers 550 channels 36 | - ~1 request per second. 37 | - Processes 500k+ casts per month. 38 | 39 | All Data APIs are usage based and highly variable. Here's a snapshot of August. 40 | | Service | Provider | Cost | Notes | 41 | |----------------------|-------------------------|----------------------------------------|----------------------------------------------------------------------| 42 | | API | fly.io | $13/mo | 2 X shared-cpu-2x with 1024 MB memory | 43 | | PostgreSQL | fly.io | $35/mo | 2 X shared-cpu-2x with 4096 MB memory (over provisioned) | 44 | | Redis | railway.app | < $1/mo | | 45 | | Farcaster Data | Neynar | > $100/mo (Contact Neynar) | Used for cast metadata, webhooks, feeds, frames, etc. | 46 | | NFT Data | SimpleHash | > $100/mo (Contact SimpleHash) | Needed for 1155 token lookups and Zora Network at high concurrency. | 47 | | Ethereum JSON Data | Alchemy/Infura/Coinbase | ~$30/mo | Most ERC20/721/1155 token lookups and Sign in With Farcaster. | 48 | | Airstack Data | Airstack | Buy 1 Fan Token, free forever | Channel Following, FarRank, FarScore | 49 | | Moxie Data | The Graph | < $5/mo | | 50 | -------------------------------------------------------------------------------- /app/components/FarcasterIcon.tsx: -------------------------------------------------------------------------------- 1 | export function FarcasterIcon(props: { className?: string }) { 2 | return ( 3 | 4 | 8 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/components/color-picker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useRef, useEffect } from "react"; 4 | import { ChevronDown } from "lucide-react"; 5 | import { Button } from "~/components/ui/button"; 6 | import { Input } from "~/components/ui/input"; 7 | 8 | export default function ColorPicker({ setColor }: { setColor: (color: string) => void }) { 9 | const [isOpen, setIsOpen] = useState(false); 10 | const [selectedColor, setSelectedColor] = useState(""); 11 | const [customColor, setCustomColor] = useState(""); 12 | const dropdownRef = useRef(null); 13 | const buttonRef = useRef(null); 14 | 15 | const presetColors = [ 16 | { code: "#ea580c", value: "#ea580c" }, 17 | { code: "#000000", value: "#000000" }, 18 | { code: "#472B82", value: "#472B82" }, 19 | { code: "#7c65c1", value: "#7c65c1" }, 20 | ]; 21 | 22 | const handleColorSelect = (color: string, value: string) => { 23 | setSelectedColor(color); 24 | setColor(value); 25 | setIsOpen(false); 26 | }; 27 | 28 | const handleCustomColorSubmit = (e: React.FormEvent) => { 29 | e.preventDefault(); 30 | if (customColor) { 31 | setSelectedColor(customColor); 32 | setColor(customColor); 33 | setIsOpen(false); 34 | } 35 | }; 36 | 37 | useEffect(() => { 38 | const handleClickOutside = (event: MouseEvent) => { 39 | if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { 40 | setIsOpen(false); 41 | } 42 | }; 43 | 44 | document.addEventListener("mousedown", handleClickOutside); 45 | return () => { 46 | document.removeEventListener("mousedown", handleClickOutside); 47 | }; 48 | }, []); 49 | 50 | return ( 51 |
52 | 61 | {isOpen && ( 62 |
66 |
67 | {presetColors.map((color) => ( 68 | 82 | ))} 83 |
84 | setCustomColor(e.target.value)} 89 | className="mb-2" 90 | /> 91 | 94 |
95 |
96 |
97 | )} 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /app/components/container.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | export function Container({ className, ...props }: React.ComponentPropsWithoutRef<"div">) { 4 | return
; 5 | } 6 | -------------------------------------------------------------------------------- /app/components/icons/farcaster.tsx: -------------------------------------------------------------------------------- 1 | export function Farcaster(props: { className: string }) { 2 | return ( 3 | 9 | 13 | 17 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/components/sub-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { NavLink } from "@remix-run/react"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | export interface SidebarNavProps extends React.HTMLAttributes { 8 | items: { 9 | to: string; 10 | title: string; 11 | end?: boolean; 12 | }[]; 13 | } 14 | 15 | export function SidebarNav({ className, items, ...props }: SidebarNavProps) { 16 | return ( 17 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 3 | import { ChevronDownIcon } from "@radix-ui/react-icons"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | const Accordion = AccordionPrimitive.Root; 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 14 | )); 15 | AccordionItem.displayName = "AccordionItem"; 16 | 17 | const AccordionTrigger = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef & { 20 | hideChevron?: boolean; 21 | } 22 | >(({ className, hideChevron, children, ...props }, ref) => ( 23 | 24 | svg]:rotate-180", 28 | className 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | {hideChevron ? null : ( 34 | 35 | )} 36 | 37 | 38 | )); 39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 40 | 41 | const AccordionContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, children, ...props }, ref) => ( 45 | 50 |
{children}
51 |
52 | )); 53 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 54 | 55 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 56 | -------------------------------------------------------------------------------- /app/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "~/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
27 | )); 28 | Alert.displayName = "Alert"; 29 | 30 | const AlertTitle = React.forwardRef>( 31 | ({ className, ...props }, ref) => ( 32 |
33 | ) 34 | ); 35 | AlertTitle.displayName = "AlertTitle"; 36 | 37 | const AlertDescription = React.forwardRef>( 38 | ({ className, ...props }, ref) => ( 39 |
40 | ) 41 | ); 42 | AlertDescription.displayName = "AlertDescription"; 43 | 44 | export { Alert, AlertTitle, AlertDescription }; 45 | -------------------------------------------------------------------------------- /app/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 3 | 4 | import { cn } from "~/lib/utils"; 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 15 | )); 16 | Avatar.displayName = AvatarPrimitive.Root.displayName; 17 | 18 | const AvatarImage = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 27 | )); 28 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 29 | 30 | const AvatarFallback = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 39 | )); 40 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 41 | 42 | export { Avatar, AvatarImage, AvatarFallback }; 43 | -------------------------------------------------------------------------------- /app/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-black/80 text-primary-foreground shadow hover:bg-black/90", 13 | destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 14 | outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 15 | secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline", 18 | }, 19 | size: { 20 | default: "h-9 px-4 py-2", 21 | xs: "h-6 rounded-sm text-xs px-2", 22 | sm: "h-8 rounded-md px-3 text-xs", 23 | lg: "h-10 rounded-md px-8", 24 | icon: "h-9 w-9", 25 | }, 26 | }, 27 | defaultVariants: { 28 | variant: "default", 29 | size: "default", 30 | }, 31 | } 32 | ); 33 | 34 | export interface ButtonProps 35 | extends React.ButtonHTMLAttributes, 36 | VariantProps { 37 | asChild?: boolean; 38 | } 39 | 40 | const Button = React.forwardRef( 41 | ({ className, variant, size, asChild = false, ...props }, ref) => { 42 | const Comp = asChild ? Slot : "button"; 43 | return ; 44 | } 45 | ); 46 | Button.displayName = "Button"; 47 | 48 | export { Button, buttonVariants }; 49 | -------------------------------------------------------------------------------- /app/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "~/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /app/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 3 | import { CheckIcon } from "@radix-ui/react-icons"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )); 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 27 | 28 | export { Checkbox }; 29 | -------------------------------------------------------------------------------- /app/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import { Cross2Icon } from "@radix-ui/react-icons"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | const Dialog = DialogPrimitive.Root; 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger; 10 | 11 | const DialogPortal = DialogPrimitive.Portal; 12 | 13 | const DialogClose = DialogPrimitive.Close; 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )); 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )); 52 | DialogContent.displayName = DialogPrimitive.Content.displayName; 53 | 54 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 55 |
56 | ); 57 | DialogHeader.displayName = "DialogHeader"; 58 | 59 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 60 |
64 | ); 65 | DialogFooter.displayName = "DialogFooter"; 66 | 67 | const DialogTitle = React.forwardRef< 68 | React.ElementRef, 69 | React.ComponentPropsWithoutRef 70 | >(({ className, ...props }, ref) => ( 71 | 76 | )); 77 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 78 | 79 | const DialogDescription = React.forwardRef< 80 | React.ElementRef, 81 | React.ComponentPropsWithoutRef 82 | >(({ className, ...props }, ref) => ( 83 | 88 | )); 89 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 90 | 91 | export { 92 | Dialog, 93 | DialogPortal, 94 | DialogOverlay, 95 | DialogTrigger, 96 | DialogClose, 97 | DialogContent, 98 | DialogHeader, 99 | DialogFooter, 100 | DialogTitle, 101 | DialogDescription, 102 | }; 103 | -------------------------------------------------------------------------------- /app/components/ui/fields.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | import { Input } from "./input"; 3 | 4 | export function FieldLabel( 5 | props: { 6 | position?: "left" | "right"; 7 | description?: string; 8 | label: React.ReactNode; 9 | labelProps?: React.LabelHTMLAttributes; 10 | } & React.HTMLAttributes 11 | ) { 12 | const _position = props.position || "left"; 13 | const _labelClassName = props.labelProps?.className || ""; 14 | const { className, ...rest } = props.labelProps || {}; 15 | 16 | return ( 17 |
18 | {_position === "left" ? ( 19 | <> 20 | 23 |
24 | {props.children} 25 | {props.description &&
{props.description}
} 26 |
27 | 28 | ) : ( 29 | <> 30 | {props.children} 31 |
32 | 35 | {props.description &&
{props.description}
} 36 |
37 | 38 | )} 39 |
40 | ); 41 | } 42 | 43 | export function SliderField( 44 | props: { 45 | label: string; 46 | description?: string | React.ReactNode; 47 | labelProps?: React.LabelHTMLAttributes; 48 | children: React.ReactNode; 49 | } & React.HTMLAttributes 50 | ) { 51 | const { children, ...rest } = props; 52 | const descriptionNode = props.description ? ( 53 | typeof props.description === "string" ? ( 54 |
{props.description}
55 | ) : ( 56 | props.description 57 | ) 58 | ) : null; 59 | 60 | return ( 61 |
68 |
69 | 78 | {descriptionNode} 79 |
80 | {children} 81 |
82 | ); 83 | } 84 | 85 | export function Field({ 86 | name, 87 | label, 88 | inputProps, 89 | ...props 90 | }: { 91 | name: string; 92 | label: string; 93 | inputProps?: React.InputHTMLAttributes; 94 | } & React.HTMLAttributes) { 95 | return ( 96 |
97 | 100 | 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /app/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const HoverCard = HoverCardPrimitive.Root 7 | 8 | const HoverCardTrigger = HoverCardPrimitive.Trigger 9 | 10 | const HoverCardContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 24 | )) 25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 26 | 27 | export { HoverCard, HoverCardTrigger, HoverCardContent } 28 | -------------------------------------------------------------------------------- /app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverAnchor = PopoverPrimitive.Anchor 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 32 | -------------------------------------------------------------------------------- /app/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { CheckIcon } from "@radix-ui/react-icons" 3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" 4 | 5 | import { cn } from "~/lib/utils" 6 | 7 | const RadioGroup = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => { 11 | return ( 12 | 17 | ) 18 | }) 19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName 20 | 21 | const RadioGroupItem = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => { 25 | return ( 26 | 34 | 35 | 36 | 37 | 38 | ) 39 | }) 40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName 41 | 42 | export { RadioGroup, RadioGroupItem } 43 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes" 2 | import { Toaster as Sonner } from "sonner" 3 | 4 | type ToasterProps = React.ComponentProps 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 26 | ) 27 | } 28 | 29 | export { Toaster } 30 | -------------------------------------------------------------------------------- /app/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import * as React from "react"; 3 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | const Switch = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 24 | 25 | )); 26 | Switch.displayName = SwitchPrimitives.Root.displayName; 27 | 28 | export { Switch }; 29 | -------------------------------------------------------------------------------- /app/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /app/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TabsPrimitive from "@radix-ui/react-tabs" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const Tabs = TabsPrimitive.Root 7 | 8 | const TabsList = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | TabsList.displayName = TabsPrimitive.List.displayName 22 | 23 | const TabsTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 35 | )) 36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 37 | 38 | const TabsContent = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 | 50 | )) 51 | TabsContent.displayName = TabsPrimitive.Content.displayName 52 | 53 | export { Tabs, TabsList, TabsTrigger, TabsContent } 54 | -------------------------------------------------------------------------------- /app/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 |