├── .eslintrc.json ├── .gitignore ├── .vscode └── launch.json ├── README.md ├── bun.lockb ├── next-auth.d.ts ├── next.config.mjs ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── logo-dark.png ├── logo-light.png ├── next.svg └── vercel.svg ├── src ├── app │ ├── api │ │ ├── auth │ │ │ ├── [...nextauth] │ │ │ │ └── route.ts │ │ │ └── signup │ │ │ │ └── route.ts │ │ ├── trpc │ │ │ └── [trpc] │ │ │ │ └── route.ts │ │ └── webhook │ │ │ └── route.ts │ ├── d │ │ ├── chats │ │ │ ├── [chatId] │ │ │ │ ├── announcements │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── moderations │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── settings │ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── settings │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ ├── page.tsx │ ├── setup │ │ └── page.tsx │ └── signup │ │ └── page.tsx ├── assets │ └── logo.svg ├── components │ ├── AnnouncementCreate.tsx │ ├── AppBar.tsx │ ├── AppDrawer.tsx │ ├── AppSidebar.tsx │ ├── AvatarMenu.tsx │ ├── ChatsList.tsx │ ├── Filters │ │ ├── Blacklist.tsx │ │ └── MessageType.tsx │ ├── InputBuilder.tsx │ ├── KeyboardButton.tsx │ ├── ModerationsList.tsx │ ├── PageTitle.tsx │ ├── ProgressBar.tsx │ ├── SectionBuilder.tsx │ ├── SectionCard.tsx │ ├── SidebarItem.tsx │ ├── TipTap.tsx │ └── UpdateBotToken.tsx ├── lib │ ├── Providers.tsx │ ├── auth.ts │ ├── bot.ts │ ├── client.ts │ ├── helpers │ │ └── UpdateAppTitle.tsx │ ├── moderations │ │ ├── basic.ts │ │ ├── filters.ts │ │ ├── ids.ts │ │ ├── new-users.ts │ │ └── types.ts │ ├── prisma.ts │ ├── routers │ │ ├── announcements.ts │ │ ├── bots.ts │ │ ├── index.ts │ │ └── ping.ts │ ├── store.ts │ ├── types.ts │ └── utils.ts ├── server │ ├── context.ts │ ├── index.ts │ └── trpc.ts └── telegram │ ├── bot.ts │ └── database.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "npm run dev", 21 | "serverReadyAction": { 22 | "pattern": "- Local:.+(https?://.+)", 23 | "uriFormat": "%s", 24 | "action": "debugWithChrome" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modzero 2 | 3 | > [!IMPORTANT] 4 | > The project is under development. Feel free to contribute and create a pull request for new features. Join our [Telegram support chat](https://t.me/modzerochat) for project discussions. 5 | 6 | ![Artboard](https://github.com/mxvh/modzero/assets/31907722/e504b081-0595-4aca-af29-48297db4b336) 7 | 8 | Mange your Telegram groups and channels from one place, packed with multiple moderation tools to help you become a pro administrator. 9 | 10 | ## ✨ Features 11 | 12 | ![Artboard](https://github.com/mxvh/modzero/assets/31907722/cb3585e7-3409-4e34-9ded-91458e307a3c) 13 | 14 | 1. 🔓 **Open Source**: Our platform is open source, meaning the underlying code is freely available for anyone to view, use, and modify. This fosters transparency, collaboration, and community-driven development. 15 | 16 | 2. 💻 **Modern and Intuitive UI**: We pride ourselves on offering a modern and intuitive user interface (UI) that is easy to navigate and aesthetically pleasing. Our UI is designed to enhance user experience and streamline the management of Telegram groups and channels. 17 | 18 | 3. 🤖 **Custom Telegram Bot**: Our platform includes a custom Telegram bot that provides advanced functionality and automation. This bot can perform various tasks, such as scheduling messages, managing member permissions, and executing moderation actions, all tailored to meet your specific needs. 19 | 20 | 4. 📈 **Unlimited Chats and Members**: With our platform, you can manage an unlimited number of Telegram chats (groups and channels) and members. Whether you're running a small community or a large organization, our platform can scale to accommodate your requirements. 21 | 22 | 5. 🛠️ **Moderation Tools**: We offer a comprehensive set of moderation tools to help you maintain a safe and enjoyable environment within your Telegram communities. From banning users and deleting messages to setting content guidelines and managing member roles, our moderation tools give you full control over your groups and channels. 23 | 24 | 6. 📢 **Announcements**: Our platform allows you to create and send announcements to your Telegram communities quickly and easily. Whether you're promoting an event, sharing important updates, or broadcasting messages to your members, our announcement feature ensures your messages reach their intended audience effectively. 25 | 26 | 7. 📊 **Analytics**: Gain valuable insights into the performance and engagement of your Telegram communities with our built-in analytics tools. Track metrics such as member growth, message engagement, and user activity to inform your community management strategies and drive growth. 27 | 28 | 8. 🚀 **Quick Install and Setup Using All Free Providers (Vercel for Hosting and Neon.tech for Database)**: We offer a hassle-free installation and setup process using free providers like Vercel for hosting and Neon.tech for database storage. With just a few simple steps, you can have your Telegram management platform up and running in no time, without any upfront costs. 29 | 30 | 31 | 32 | ## ⚡ Installation 33 | 34 | ### Step 1: Fork the Repository 35 | Start by forking the repository into your GitHub account. This creates a copy of the original repository under your account, allowing you to make changes without affecting the original project. 36 | 37 | ### Step 2: Create Accounts 38 | Sign up for accounts on Vercel and Neon.tech if you haven't already. These platforms will be used to host and deploy your application. 39 | 40 | ### Step 3: Add Repository to Vercel 41 | Navigate to Vercel and add your forked repository. Vercel provides an intuitive interface for deploying and managing web applications. 42 | 43 | ### Step 4: Set Environment Variables 44 | Fill in the required environment variables to configure your application: 45 | 46 | ```env 47 | WEBHOOK_ADDRESS: This is the address where your application's webhook will be hosted. Replace your_vercel_address with the address provided by Vercel. 48 | DATABASE_URL: This is the URL of your database hosted on Neon.tech. 49 | NEXTAUTH_SECRET: Use ` openssl rand -base64 32` to generate random string. 50 | ``` 51 | 52 | ## 🚗 Roadmap 53 | 54 | To checkout roadmap of the project, please visit the project section of the repository. 55 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxvsh/modzero/7e7eb9cdcefa318b9a74419b17c611441ba650a7/bun.lockb -------------------------------------------------------------------------------- /next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { DefaultSession } from 'next-auth'; 2 | import { User } from '@prisma/client'; 3 | 4 | declare module 'next-auth' { 5 | /** 6 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 7 | */ 8 | interface Session { 9 | user: User & DefaultSession['user']; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'images.unsplash.com', 8 | pathname: '**', 9 | }, 10 | ], 11 | }, 12 | }; 13 | 14 | export default nextConfig; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mod0-pro", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "prisma db push && next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.3.4", 13 | "@nextui-org/react": "^2.2.9", 14 | "@prisma/client": "^5.9.1", 15 | "@radix-ui/react-label": "^2.0.2", 16 | "@radix-ui/react-select": "^2.0.0", 17 | "@radix-ui/react-slot": "^1.0.2", 18 | "@tanstack/react-query": "4.35.3", 19 | "@tiptap/react": "^2.2.2", 20 | "@tiptap/starter-kit": "^2.2.2", 21 | "@trpc/client": "^10.45.1", 22 | "@trpc/react-query": "^10.45.1", 23 | "@trpc/server": "^10.45.1", 24 | "@wcj/html-to-markdown": "^2.0.0", 25 | "argon2": "^0.31.2", 26 | "class-variance-authority": "^0.7.0", 27 | "clsx": "^2.1.0", 28 | "envalid": "^8.0.0", 29 | "framer-motion": "^11.0.3", 30 | "lucide-react": "^0.323.0", 31 | "next": "14.1.0", 32 | "next-auth": "^4.24.5", 33 | "next-nprogress-bar": "^2.1.2", 34 | "next-themes": "^0.2.1", 35 | "react": "^18", 36 | "react-dom": "^18", 37 | "react-hook-form": "^7.50.1", 38 | "react-icons": "^5.0.1", 39 | "sonner": "^1.4.0", 40 | "tailwind-merge": "^2.2.1", 41 | "tailwindcss-animate": "^1.0.7", 42 | "telegraf": "^4.15.3", 43 | "zod": "^3.22.4", 44 | "zustand": "^4.5.0" 45 | }, 46 | "devDependencies": { 47 | "@types/node": "^20", 48 | "@types/react": "^18", 49 | "@types/react-dom": "^18", 50 | "autoprefixer": "^10.0.1", 51 | "eslint": "^8", 52 | "eslint-config-next": "14.1.0", 53 | "postcss": "^8", 54 | "prisma": "^5.9.1", 55 | "tailwindcss": "^3.3.0", 56 | "typescript": "^5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id String @id @default(cuid()) 15 | username String @unique 16 | passwordHash String @map("password_hash") 17 | role String @default("user") 18 | 19 | createdAt DateTime @default(now()) @map("created_at") 20 | updatedAt DateTime @updatedAt @map("updated_at") 21 | 22 | bots Bot[] 23 | } 24 | 25 | model Bot { 26 | id String @id 27 | name String 28 | token String 29 | username String @unique 30 | botInfo Json @map("bot_info") 31 | 32 | createdAt DateTime @default(now()) @map("created_at") 33 | updatedAt DateTime @updatedAt @map("updated_at") 34 | 35 | user User @relation(fields: [userId], references: [id]) 36 | userId String 37 | } 38 | 39 | model TelegramUser { 40 | id String @id 41 | 42 | firstName String @map("first_name") 43 | lastName String? @map("last_name") 44 | username String? @map("username") 45 | 46 | telegramUserChats TelegramUserChat[] 47 | 48 | @@map("telegram_users") 49 | } 50 | 51 | model TelegramChat { 52 | id String @id 53 | 54 | title String 55 | type String 56 | admins Json @default("[]") 57 | totalMembers Int @map("total_members") 58 | 59 | createdAt DateTime @default(now()) @map("created_at") 60 | updatedAt DateTime @updatedAt @map("updated_at") 61 | 62 | telegramUserChats TelegramUserChat[] 63 | 64 | @@map("telegram_chats") 65 | } 66 | 67 | model TelegramUserChat { 68 | id String @id @default(cuid()) 69 | 70 | userId String @map("user_id") 71 | chatId String @map("chat_id") 72 | 73 | user TelegramUser @relation(fields: [userId], references: [id]) 74 | chat TelegramChat @relation(fields: [chatId], references: [id]) 75 | 76 | createdAt DateTime @default(now()) @map("created_at") 77 | updatedAt DateTime @updatedAt @map("updated_at") 78 | 79 | @@unique([userId, chatId]) 80 | @@map("telegram_user_chats") 81 | } 82 | -------------------------------------------------------------------------------- /public/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxvsh/modzero/7e7eb9cdcefa318b9a74419b17c611441ba650a7/public/logo-dark.png -------------------------------------------------------------------------------- /public/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxvsh/modzero/7e7eb9cdcefa318b9a74419b17c611441ba650a7/public/logo-light.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import { authOptions } from '~lib/auth'; 3 | 4 | const handler = NextAuth(authOptions); 5 | 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /src/app/api/auth/signup/route.ts: -------------------------------------------------------------------------------- 1 | import argon from 'argon2'; 2 | import prisma from '~lib/prisma'; 3 | 4 | export async function POST(req: Request) { 5 | const payload = await req.json(); 6 | 7 | // todo: Add zod validation 8 | 9 | try { 10 | const isFirst = (await prisma.user.count()) === 0; 11 | 12 | if (!isFirst) { 13 | return new Response('Signups are disabled', { status: 403 }); 14 | } 15 | 16 | const hash = await argon.hash(payload.password); 17 | 18 | await prisma.user.create({ 19 | data: { 20 | username: payload.username, 21 | passwordHash: hash, 22 | }, 23 | }); 24 | } catch (error) { 25 | console.error(error); 26 | return new Response('error', { status: 500 }); 27 | } 28 | 29 | return new Response('ok'); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; 2 | import { appRouter } from '~server/index'; 3 | import { createContext } from '~server/context'; 4 | 5 | const handler = (req: Request) => 6 | fetchRequestHandler({ 7 | endpoint: '/api/trpc', 8 | req, 9 | router: appRouter, 10 | createContext, 11 | }); 12 | 13 | export { handler as GET, handler as POST }; 14 | -------------------------------------------------------------------------------- /src/app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { Telegraf } from 'telegraf'; 2 | import prisma from '~lib/prisma'; 3 | import telegramBot from '~telegram/bot'; 4 | 5 | export async function POST(req: Request) { 6 | const url = new URL(req.url); 7 | const payload = await req.json(); 8 | 9 | const botId = url.searchParams.get('botId'); 10 | 11 | const botData = await prisma.bot.findUnique({ 12 | where: { 13 | id: botId as string, 14 | }, 15 | }); 16 | 17 | if (!botData) { 18 | throw new Error('Bot not found'); 19 | } 20 | 21 | const bot = new Telegraf(botData.token, {}); 22 | 23 | bot.use(telegramBot); 24 | 25 | await bot.handleUpdate(payload); 26 | 27 | return new Response('ok'); 28 | } 29 | 30 | export function GET() { 31 | return new Response('ok'); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/d/chats/[chatId]/announcements/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AnnouncementCreate } from '~components/AnnouncementCreate'; 3 | import PageTitle from '~components/PageTitle'; 4 | 5 | function Annoucements({ 6 | params, 7 | }: { 8 | params: { 9 | chatId: string; 10 | }; 11 | }) { 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | } 19 | 20 | export default Annoucements; 21 | -------------------------------------------------------------------------------- /src/app/d/chats/[chatId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { redirect } from 'next/navigation'; 3 | import UpdateAppTitle from '~lib/helpers/UpdateAppTitle'; 4 | import prisma from '~lib/prisma'; 5 | 6 | async function ChatLayout({ 7 | children, 8 | params, 9 | }: { 10 | children: React.ReactNode; 11 | params: { 12 | chatId: string; 13 | }; 14 | }) { 15 | const chatData = await prisma.telegramChat.findUnique({ 16 | where: { 17 | id: params.chatId, 18 | }, 19 | }); 20 | 21 | if (!chatData) { 22 | redirect('/d/chats'); 23 | } 24 | 25 | return ( 26 | <> 27 | {children} 28 | 29 | 30 | ); 31 | } 32 | 33 | export default ChatLayout; 34 | -------------------------------------------------------------------------------- /src/app/d/chats/[chatId]/moderations/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ModerationsList from '~components/ModerationsList'; 3 | import PageTitle from '~components/PageTitle'; 4 | 5 | function Moderations() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default Moderations; 15 | -------------------------------------------------------------------------------- /src/app/d/chats/[chatId]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageTitle from '~components/PageTitle'; 3 | 4 | function Home() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default Home; 13 | -------------------------------------------------------------------------------- /src/app/d/chats/[chatId]/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageTitle from '~components/PageTitle'; 3 | 4 | function Settings() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default Settings; 13 | -------------------------------------------------------------------------------- /src/app/d/chats/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChatsList from '~components/ChatsList'; 3 | import PageTitle from '~components/PageTitle'; 4 | import prisma from '~lib/prisma'; 5 | 6 | async function Chats() { 7 | const chats = await prisma.telegramChat.findMany(); 8 | 9 | return ( 10 | <> 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default Chats; 18 | -------------------------------------------------------------------------------- /src/app/d/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { getServerSession } from 'next-auth'; 4 | import { redirect } from 'next/navigation'; 5 | import { AppBar } from '~components/AppBar'; 6 | import { AppSidebar } from '~components/AppSidebar'; 7 | import { authOptions } from '~lib/auth'; 8 | import { getUserBots } from '~lib/bot'; 9 | import ProgressBar from '~components/ProgressBar'; 10 | 11 | async function ChatsLayout({ children }: { children: React.ReactNode }) { 12 | const session = await getServerSession(authOptions); 13 | 14 | if (!session) { 15 | redirect('/login'); 16 | } 17 | 18 | const bots = await getUserBots(session.user.id); 19 | 20 | if (bots.length === 0) { 21 | redirect('/setup'); 22 | } 23 | 24 | return ( 25 |
26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 |
34 |
{children}
35 |
36 |
37 |
38 | ); 39 | } 40 | 41 | export default ChatsLayout; 42 | -------------------------------------------------------------------------------- /src/app/d/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getServerSession } from 'next-auth/next'; 3 | import UpdateBotToken from '~components/UpdateBotToken'; 4 | import { authOptions } from '~lib/auth'; 5 | import { getUserBots } from '~lib/bot'; 6 | import PageTitle from '~components/PageTitle'; 7 | 8 | async function Settings() { 9 | const session = await getServerSession(authOptions); 10 | const bots = await getUserBots(session!.user.id); 11 | 12 | // read first bot 13 | const bot = bots[0]; 14 | 15 | return ( 16 | <> 17 | 18 |
19 | 20 |
21 | 22 | ); 23 | } 24 | 25 | export default Settings; 26 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxvsh/modzero/7e7eb9cdcefa318b9a74419b17c611441ba650a7/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | *::-webkit-scrollbar { 6 | width: 10px; 7 | } 8 | 9 | *::-webkit-scrollbar-thumb { 10 | background-color: #f1f1f1; 11 | border-radius: 14px; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css'; 2 | import type { Metadata, Viewport } from 'next'; 3 | import { Session } from 'next-auth'; 4 | import { Poppins } from 'next/font/google'; 5 | import Providers from '~lib/Providers'; 6 | 7 | const poppins = Poppins({ subsets: ['latin'], weight: ['400', '600'] }); 8 | 9 | export const metadata: Metadata = { 10 | title: 'modzero', 11 | description: 'Telegram Moderation Platform', 12 | }; 13 | 14 | export const viewport: Viewport = { 15 | width: 'device-width', 16 | initialScale: 1, 17 | }; 18 | 19 | type DefaultLayoutProps = { 20 | children: React.ReactNode; 21 | }; 22 | type Props = { 23 | children: React.ReactNode; 24 | session: Session; 25 | }; 26 | export default async function RootLayout(props: DefaultLayoutProps | Props) { 27 | const { children, session } = { 28 | ...props, 29 | session: undefined, 30 | }; 31 | 32 | return ( 33 | 34 | 35 | {children} 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | import { signIn } from 'next-auth/react'; 6 | import { useForm } from 'react-hook-form'; 7 | import { toast } from 'sonner'; 8 | import { Input, Button } from '@nextui-org/react'; 9 | import { useState } from 'react'; 10 | 11 | function LoginPage() { 12 | const form = useForm(); 13 | const [loading, setLoading] = useState(false); 14 | 15 | function handleLogin(data: Record) { 16 | setLoading(true); 17 | signIn('credentials', { 18 | username: data.username, 19 | password: data.password, 20 | redirect: false, 21 | }).then((res) => { 22 | if (!res?.ok) { 23 | toast.error('Invalid username or password'); 24 | setLoading(false); 25 | } else { 26 | toast.success('Login successful'); 27 | 28 | setTimeout(() => { 29 | window.location.href = '/d/chats'; 30 | }, 1000); 31 | } 32 | }); 33 | } 34 | 35 | return ( 36 |
37 |
38 |
39 | logo 46 |
47 |

48 | Log in to your account 49 |

50 |

Please log in to continue.

51 |
52 |
53 |
54 |
55 | 56 | 62 |
63 |
64 | 65 | 71 |
72 | 80 | 81 | 89 |
90 |
91 |
92 | ); 93 | } 94 | 95 | export default LoginPage; 96 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@nextui-org/react'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | Next.js Logo 16 | 17 |

modzero

18 | 19 |
20 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/setup/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Input, Button } from '@nextui-org/react'; 4 | import Image from 'next/image'; 5 | import { useRouter } from 'next/navigation'; 6 | import React from 'react'; 7 | import { useForm } from 'react-hook-form'; 8 | import { toast } from 'sonner'; 9 | import { trpc } from '~lib/client'; 10 | 11 | function SetupPage() { 12 | const r = useRouter(); 13 | const form = useForm(); 14 | const addBot = trpc.addBot.useMutation(); 15 | 16 | const handleSubmit = (data: any) => { 17 | addBot 18 | .mutateAsync(data) 19 | .then(() => { 20 | r.push('/d/chats'); 21 | }) 22 | .catch((err) => { 23 | if (err instanceof Error) { 24 | toast.error(err.message); 25 | } 26 | }); 27 | }; 28 | 29 | return ( 30 |
31 |
32 |
33 | logo 40 |
41 |

Setup your bot

42 |

You can find bot token from @BotFather

43 |
44 |
45 |
46 |
47 | 48 | 55 |
56 | 64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | export default SetupPage; 71 | -------------------------------------------------------------------------------- /src/app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | import { signIn } from 'next-auth/react'; 6 | import { useForm } from 'react-hook-form'; 7 | import { Input, Button } from '@nextui-org/react'; 8 | 9 | import { toast } from 'sonner'; 10 | import { useState } from 'react'; 11 | 12 | function SignupPage() { 13 | const form = useForm(); 14 | const [loading, setLoading] = useState(false); 15 | 16 | function handleSignup(data: Record) { 17 | setLoading(true); 18 | fetch('/api/auth/signup', { 19 | method: 'POST', 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | }, 23 | body: JSON.stringify(data), 24 | }).then(async (res) => { 25 | if (res.status === 200) { 26 | return signIn('credentials', { callbackUrl: '/bots' }); 27 | } else { 28 | toast.error(await res.text()); 29 | setLoading(false); 30 | } 31 | }); 32 | } 33 | 34 | return ( 35 |
36 |
37 |
38 | logo 45 |
46 |

47 | Create an account 48 |

49 |

Please create an account to continue.

50 |
51 |
52 |
53 |
54 | 55 | 61 |
62 |
63 | 64 | 70 |
71 | 79 | 80 | 83 |
84 |
85 |
86 | ); 87 | } 88 | 89 | export default SignupPage; 90 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | logo 4 | 8 | -------------------------------------------------------------------------------- /src/components/AnnouncementCreate.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@nextui-org/react'; 4 | import { useEditor, EditorContent, Editor } from '@tiptap/react'; 5 | import StarterKit from '@tiptap/starter-kit'; 6 | import { BoldIcon, CalendarIcon, ItalicIcon, SendIcon } from 'lucide-react'; 7 | import htmlToMarkdown from '@wcj/html-to-markdown'; 8 | import { trpc } from '~lib/client'; 9 | 10 | type ToolbarProps = { 11 | editor: Editor; 12 | }; 13 | const Toolbar = ({ editor }: ToolbarProps) => { 14 | return ( 15 |
16 |
33 | ); 34 | }; 35 | 36 | type Props = { 37 | chatId: string; 38 | }; 39 | function AnnouncementCreate({ chatId }: Props) { 40 | const createAnnouncement = trpc.createAnnouncement.useMutation(); 41 | 42 | const editor = useEditor({ 43 | extensions: [StarterKit], 44 | content: '

Hello World! 🌎️

', 45 | editorProps: { 46 | attributes: { 47 | class: 48 | 'max-h-[300px] overflow-y-auto relative w-full h-full focus:outline-none focus:ring-0 focus:ring-transparent border p-4 rounded-xl', 49 | }, 50 | }, 51 | }); 52 | 53 | async function createPost() { 54 | const html = editor?.getHTML(); 55 | const markdown = await htmlToMarkdown({ html }); 56 | 57 | if (markdown) 58 | createAnnouncement.mutate({ 59 | text: markdown, 60 | chatId, 61 | }); 62 | } 63 | 64 | if (!editor) { 65 | return null; 66 | } 67 | 68 | return ( 69 |
70 |

71 | Announcements are a great way to share important information with your 72 | community. 73 |

74 | 75 |
76 |
77 |
78 | 79 |
80 | 81 |
82 | 83 |

84 | You can use Markdown to format your text. 85 |

86 |
87 | 95 | 96 | 105 |
106 |
107 |
108 |
109 |
110 | ); 111 | } 112 | 113 | export { AnnouncementCreate }; 114 | -------------------------------------------------------------------------------- /src/components/AppBar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import { useParams } from 'next/navigation'; 5 | import { ArrowLeftIcon, BookIcon, MenuIcon, UsersIcon } from 'lucide-react'; 6 | import { Button, Spinner, useDisclosure } from '@nextui-org/react'; 7 | import Link from 'next/link'; 8 | import AvatarMenu from './AvatarMenu'; 9 | import { useAppStore } from '~lib/store'; 10 | import logo from '~assets/logo.svg'; 11 | import AppDrawer from './AppDrawer'; 12 | 13 | export function AppBar() { 14 | const params = useParams(); 15 | const appStore = useAppStore(); 16 | const isRoot = Object.keys(params).length === 0; 17 | const drawer = useDisclosure(); 18 | 19 | return ( 20 |
21 |
22 | 23 | 24 |
25 | 35 | 36 | logo 44 | 45 | {!isRoot && ( 46 |
47 | 48 | 49 | 50 | {appStore.appTitle ? ( 51 |

{appStore.appTitle}

52 | ) : ( 53 | 54 | )} 55 |
56 | )} 57 | 58 |
59 |
60 | <> 61 | 68 | 78 | 79 |
80 | 81 |
82 |
83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/components/AppDrawer.tsx: -------------------------------------------------------------------------------- 1 | import { Button, UseDisclosureProps } from '@nextui-org/react'; 2 | import React from 'react'; 3 | import { RootSidebar } from './AppSidebar'; 4 | import { TbX } from 'react-icons/tb'; 5 | 6 | type Props = { 7 | disclosure: UseDisclosureProps; 8 | }; 9 | function AppDrawer({ disclosure }: Props) { 10 | return ( 11 |
18 |
19 | 27 |
28 | 29 |
30 | ); 31 | } 32 | 33 | export default AppDrawer; 34 | -------------------------------------------------------------------------------- /src/components/AppSidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useMemo } from 'react'; 4 | import { useParams, usePathname } from 'next/navigation'; 5 | import { 6 | TbHammer, 7 | TbHome2, 8 | TbSettings, 9 | TbSettings2, 10 | TbSpeakerphone, 11 | } from 'react-icons/tb'; 12 | import { IconType } from 'react-icons/lib'; 13 | import Link from 'next/link'; 14 | 15 | type SidebarItemProps = { 16 | label: string; 17 | icon: IconType; 18 | path: string; 19 | regex: RegExp; 20 | 21 | child?: SidebarItemProps[]; 22 | childRegex?: RegExp; 23 | }; 24 | 25 | const AppSidebarItems: SidebarItemProps[] = [ 26 | { 27 | label: 'Chats', 28 | icon: TbHome2, 29 | path: '/d/chats', 30 | regex: /^\/d\/chats/, 31 | childRegex: /^\/d\/chats\/-?\d+/, 32 | child: [ 33 | { 34 | label: 'Home', 35 | icon: TbHome2, 36 | path: '/d/chats/[chatId]', 37 | regex: /^\/d\/chats\/-?\d+$/, 38 | }, 39 | { 40 | label: 'Announcements', 41 | icon: TbSpeakerphone, 42 | path: '/d/chats/[chatId]/announcements', 43 | regex: /^\/d\/chats\/-?\d+\/announcements$/, 44 | }, 45 | { 46 | label: 'Moderations', 47 | icon: TbHammer, 48 | path: '/d/chats/[chatId]/moderations', 49 | regex: /^\/d\/chats\/-?\d+\/moderations$/, 50 | }, 51 | { 52 | label: 'Settings', 53 | icon: TbSettings2, 54 | path: '/d/chats/[chatId]/settings', 55 | regex: /^\/d\/chats\/-?\d+\/settings$/, 56 | }, 57 | ], 58 | }, 59 | { 60 | label: 'Settings', 61 | icon: TbSettings, 62 | path: '/d/settings', 63 | regex: /^\/d\/settings/, 64 | }, 65 | ]; 66 | 67 | export function RootSidebar() { 68 | const params = useParams(); 69 | const pathname = usePathname(); 70 | 71 | const items = useMemo(() => { 72 | const activeItem = AppSidebarItems.find((item) => 73 | item.regex.test(pathname) 74 | ); 75 | if (!activeItem) return AppSidebarItems; 76 | 77 | if (activeItem.childRegex?.test(pathname)) { 78 | const activeChild = activeItem.child?.find((item) => 79 | item.regex.test(pathname) 80 | ); 81 | return activeItem.child ?? []; 82 | } 83 | 84 | return AppSidebarItems; 85 | }, [pathname]); 86 | 87 | return ( 88 |
89 | {items.map(({ label, icon, regex, path }) => { 90 | const Icon = icon; 91 | const isActive = regex.test(pathname); 92 | 93 | path = path.replace('[chatId]', params.chatId as string); 94 | 95 | return ( 96 | 97 |
102 |
103 | 104 |
105 |
{label}
106 |
107 | 108 | ); 109 | })} 110 |
111 | ); 112 | } 113 | 114 | export function AppSidebar() { 115 | return ( 116 |
117 | 118 |
119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /src/components/AvatarMenu.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Dropdown, 5 | DropdownTrigger, 6 | Avatar, 7 | DropdownMenu, 8 | DropdownItem, 9 | } from '@nextui-org/react'; 10 | import { useSession } from 'next-auth/react'; 11 | 12 | export default function AvatarMenu() { 13 | const { data } = useSession(); 14 | const { image, username } = data?.user || {}; 15 | 16 | return ( 17 | 18 | 19 | 25 | 26 | 27 | 28 |

Signed in as

29 |

{username}

30 |
31 | My Settings 32 | Team Settings 33 | Analytics 34 | System 35 | Configurations 36 | Help & Feedback 37 | 38 | Log Out 39 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ChatsList.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Button, 5 | Card, 6 | CardBody, 7 | CardHeader, 8 | Chip, 9 | Input, 10 | Tab, 11 | Table, 12 | TableBody, 13 | TableCell, 14 | TableColumn, 15 | TableHeader, 16 | TableRow, 17 | Tabs, 18 | } from '@nextui-org/react'; 19 | import { TelegramChat } from '@prisma/client'; 20 | import Link from 'next/link'; 21 | import React from 'react'; 22 | 23 | type Props = { 24 | chats: TelegramChat[]; 25 | }; 26 | function ChatsList({ chats }: Props) { 27 | return ( 28 |
29 |
30 | 36 |
37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | {chats.map((chat) => { 46 | return ( 47 | 48 | 49 |

{chat.title}

50 | 51 |
52 | 53 | {chat.type} 54 | 55 | 56 | {chat.id} 57 | 58 |
59 |
60 | 67 | 68 |
69 |
70 |
71 | ); 72 | })} 73 |
74 |
75 | ); 76 | } 77 | 78 | export default ChatsList; 79 | -------------------------------------------------------------------------------- /src/components/Filters/Blacklist.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Chip, Input } from '@nextui-org/react'; 2 | import { useState } from 'react'; 3 | import { LuX } from 'react-icons/lu'; 4 | import SectionCard from '~components/SectionCard'; 5 | 6 | export function BlacklistWords() { 7 | const [word, setWord] = useState(''); 8 | const [words, setWords] = useState([ 9 | 'blacklisted', 10 | 'words', 11 | 'here', 12 | ]); 13 | 14 | return ( 15 | 19 | setWord(e.target.value)} 25 | onKeyDown={(e) => { 26 | if (e.key === 'Enter') { 27 | e.preventDefault(); 28 | setWords([...words, word]); 29 | setWord(''); 30 | } 31 | }} 32 | /> 33 | 34 |
35 | {words.map((word, index) => ( 36 | { 44 | const newWords = words.filter((_, i) => i !== index); 45 | setWords(newWords); 46 | }} 47 | /> 48 | } 49 | > 50 | {word} 51 | 52 | ))} 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Filters/MessageType.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Checkbox, 4 | Modal, 5 | ModalBody, 6 | ModalContent, 7 | ModalFooter, 8 | ModalHeader, 9 | useDisclosure, 10 | } from '@nextui-org/react'; 11 | import { ExceptionInputs, MessageType } from '~lib/moderations/filters'; 12 | import { LuCog } from 'react-icons/lu'; 13 | import { useState } from 'react'; 14 | import { InputType } from '~lib/types'; 15 | import InputBuilder from '~components/InputBuilder'; 16 | import SectionCard from '~components/SectionCard'; 17 | 18 | function FilterMessageType() { 19 | const modal = useDisclosure(); 20 | const [inputs, setInputs] = useState(null); 21 | 22 | return ( 23 | 27 | <> 28 | {Object.entries(MessageType).map(([, value]) => { 29 | const exceptionInputs = ExceptionInputs[value]; 30 | return ( 31 |
32 |

{value}

33 |
34 | 35 | {exceptionInputs && ( 36 | 48 | )} 49 | 50 |
51 | ); 52 | })} 53 | 54 | 55 | 56 | {(onClose) => ( 57 | <> 58 | 59 | Exception Inputs 60 | 61 | 62 | {inputs?.map((input) => ( 63 | 64 | ))} 65 | 66 | 67 | 70 | 73 | 74 | 75 | )} 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | 83 | export default FilterMessageType; 84 | -------------------------------------------------------------------------------- /src/components/InputBuilder.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, Input } from '@nextui-org/react'; 2 | import { TipTap } from './TipTap'; 3 | import { KeyboardButtonBuilder } from './KeyboardButton'; 4 | import { InputType } from '~lib/types'; 5 | 6 | type InputBuilderProps = { 7 | input: InputType; 8 | }; 9 | const InputBuilder = ({ input }: InputBuilderProps) => { 10 | switch (input.type) { 11 | case 'checkbox': { 12 | return ( 13 |
14 | {input.label} 15 | {input.hint &&

{input.hint}

} 16 |
17 | ); 18 | } 19 | 20 | case 'text': { 21 | return ( 22 |
23 | 32 | {input.hint &&

{input.hint}

} 33 |
34 | ); 35 | } 36 | 37 | case 'markdown': { 38 | return ( 39 |
40 | 41 |
42 | ); 43 | } 44 | 45 | case 'url_buttons': { 46 | return ( 47 |
48 | 49 |
50 | ); 51 | } 52 | 53 | default: { 54 | return
Unknown input type
; 55 | } 56 | } 57 | }; 58 | 59 | export default InputBuilder; 60 | -------------------------------------------------------------------------------- /src/components/KeyboardButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Input, 4 | Modal, 5 | ModalBody, 6 | ModalContent, 7 | ModalFooter, 8 | ModalHeader, 9 | useDisclosure, 10 | } from '@nextui-org/react'; 11 | import { useRef, useState } from 'react'; 12 | import { TbPlus } from 'react-icons/tb'; 13 | import { InlineKeyboardButton } from 'telegraf/types'; 14 | 15 | export function KeyboardButtonBuilder() { 16 | const modal = useDisclosure({}); 17 | const [buttons, setButtons] = useState( 18 | [] 19 | ); 20 | const [entry, setEntry] = useState( 21 | null 22 | ); 23 | const [entryIndex, setEntryIndex] = useState<[number, number] | null>(null); 24 | const [isNewEntry, setIsNewEntry] = useState(false); 25 | 26 | const textRef = useRef(null); 27 | const urlRef = useRef(null); 28 | 29 | return ( 30 |
31 | 34 | 39 | 40 | {() => ( 41 | <> 42 | Buttons 43 | 44 | {entry ? ( 45 |
46 | 52 | 58 |
59 | 79 | 99 |
100 |
101 | ) : null} 102 | 103 |
104 | {buttons.map((cols, index) => ( 105 |
106 | {cols.map((b) => ( 107 |
108 | 120 |
121 | ))} 122 | 134 |
135 | ))} 136 | 137 | 149 |
150 |
151 | 152 | 153 | )} 154 |
155 |
156 |
157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/components/ModerationsList.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Tab, Tabs } from '@nextui-org/react'; 4 | import React from 'react'; 5 | import { SectionBuilder } from './SectionBuilder'; 6 | import FilterMessageType from './Filters/MessageType'; 7 | import { BlacklistWords } from './Filters/Blacklist'; 8 | import { BasicModeration } from '~lib/moderations/basic'; 9 | import { NewUserModerations } from '~lib/moderations/new-users'; 10 | import { FilterModerations } from '~lib/moderations/filters'; 11 | 12 | function ModerationsList() { 13 | return ( 14 |
15 | 26 | 27 |
28 | {BasicModeration.map((section) => ( 29 | 30 | ))} 31 |
32 |
33 | 34 |
35 | {NewUserModerations.map((section) => ( 36 | 37 | ))} 38 |
39 |
40 | 41 |
42 | 43 | {FilterModerations.map((section) => ( 44 | 45 | ))} 46 | 47 |
48 |
49 | 50 | 51 |
52 |
53 | ); 54 | } 55 | 56 | export default ModerationsList; 57 | -------------------------------------------------------------------------------- /src/components/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function PageTitle({ 4 | title, 5 | extra, 6 | }: { 7 | title: string; 8 | extra?: React.ReactNode; 9 | }) { 10 | return ( 11 |
12 |

{title}

13 |
14 | {extra} 15 |
16 | ); 17 | } 18 | 19 | export default PageTitle; 20 | -------------------------------------------------------------------------------- /src/components/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { AppProgressBar } from 'next-nprogress-bar'; 5 | 6 | function ProgressBar() { 7 | return ( 8 | 14 | ); 15 | } 16 | 17 | export default ProgressBar; 18 | -------------------------------------------------------------------------------- /src/components/SectionBuilder.tsx: -------------------------------------------------------------------------------- 1 | import { ModerationSection } from '~lib/moderations/types'; 2 | import InputBuilder from './InputBuilder'; 3 | 4 | type FormBuilderProps = { 5 | section: ModerationSection; 6 | }; 7 | function SectionBuilder({ section }: FormBuilderProps) { 8 | return ( 9 | <> 10 |
11 |
12 |

{section.title}

13 |

{section.subTitle}

14 |
15 | 16 |
17 | {section.inputs.map((input) => ( 18 | 19 | ))} 20 |
21 |
22 | 23 | ); 24 | } 25 | 26 | export { SectionBuilder }; 27 | -------------------------------------------------------------------------------- /src/components/SectionCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | title: string; 5 | description: string; 6 | children: React.ReactNode; 7 | }; 8 | function SectionCard({ title, description, children }: Props) { 9 | return ( 10 |
11 |

{title}

12 |

{description}

13 | 14 |
{children}
15 |
16 | ); 17 | } 18 | 19 | export default SectionCard; 20 | -------------------------------------------------------------------------------- /src/components/SidebarItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function SidebarItem() { 4 | return
SidebarItem
; 5 | } 6 | 7 | export default SidebarItem; 8 | -------------------------------------------------------------------------------- /src/components/TipTap.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@nextui-org/react'; 4 | import { useEditor, EditorContent, Editor } from '@tiptap/react'; 5 | import StarterKit from '@tiptap/starter-kit'; 6 | import { BoldIcon, ItalicIcon } from 'lucide-react'; 7 | 8 | type ToolbarProps = { 9 | editor: Editor; 10 | }; 11 | const Toolbar = ({ editor }: ToolbarProps) => { 12 | return ( 13 |
14 |
31 | ); 32 | }; 33 | 34 | type TipTapProps = { 35 | hint?: string; 36 | }; 37 | function TipTap({ hint }: TipTapProps) { 38 | const editor = useEditor({ 39 | extensions: [StarterKit], 40 | content: '

Hello World! 🌎️

', 41 | editorProps: { 42 | attributes: { 43 | class: 44 | 'max-h-[300px] overflow-y-auto relative w-full h-full focus:outline-none focus:ring-0 focus:ring-transparent border p-4 rounded-xl', 45 | }, 46 | }, 47 | }); 48 | 49 | if (!editor) { 50 | return null; 51 | } 52 | 53 | return ( 54 |
55 |
56 |
57 | 58 |
59 | 60 |
61 | 62 |

63 | {hint || 'You can use Markdown to format your text'} 64 |

65 |
66 |
67 |
68 | ); 69 | } 70 | 71 | export { TipTap }; 72 | -------------------------------------------------------------------------------- /src/components/UpdateBotToken.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { 5 | Avatar, 6 | Button, 7 | Card, 8 | CardBody, 9 | CardFooter, 10 | CardHeader, 11 | Chip, 12 | Input, 13 | } from '@nextui-org/react'; 14 | import { Bot } from '@prisma/client'; 15 | import { TbDeviceFloppy, TbRefresh } from 'react-icons/tb'; 16 | import { trpc } from '~lib/client'; 17 | 18 | type Props = { 19 | bot: Bot; 20 | }; 21 | function UpdateBotToken({ bot }: Props) { 22 | const restartBot = trpc.restartBot.useMutation(); 23 | 24 | return ( 25 | 26 | 27 |
28 | 32 |
33 |

{bot.name}

34 |

{bot.id}

35 |
36 |
37 |
38 | 39 | 44 | 45 | 46 | 49 | 61 | 62 |
63 | ); 64 | } 65 | 66 | export default UpdateBotToken; 67 | -------------------------------------------------------------------------------- /src/lib/Providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { ThemeProvider } from 'next-themes'; 5 | import { NextUIProvider } from '@nextui-org/react'; 6 | import { SessionProvider } from 'next-auth/react'; 7 | import { Toaster } from 'sonner'; 8 | import { Session } from 'next-auth'; 9 | 10 | import { trpc } from './client'; 11 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 12 | import { httpBatchLink } from '@trpc/client'; 13 | import { useState } from 'react'; 14 | 15 | function TrpcProvider({ children }: { children: React.ReactNode }) { 16 | const [queryClient] = useState(() => new QueryClient()); 17 | const [trpcClient] = useState(() => 18 | trpc.createClient({ 19 | links: [ 20 | httpBatchLink({ 21 | url: '/api/trpc', 22 | }), 23 | ], 24 | }) 25 | ); 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | } 33 | 34 | function Providers({ 35 | children, 36 | session, 37 | }: { 38 | children: React.ReactNode; 39 | session: Session; 40 | }) { 41 | return ( 42 | 47 | 48 | 49 | 50 | {children} 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | export default Providers; 58 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthOptions } from 'next-auth'; 2 | import Credentials from 'next-auth/providers/credentials'; 3 | import argon from 'argon2'; 4 | import prisma from './prisma'; 5 | 6 | export const authOptions: AuthOptions = { 7 | // Configure one or more authentication providers 8 | providers: [ 9 | Credentials({ 10 | async authorize(credentials) { 11 | if (!credentials) { 12 | return null; 13 | } 14 | 15 | const { username, password } = credentials; 16 | 17 | const user = await prisma.user.findFirst({ 18 | where: { 19 | username, 20 | }, 21 | }); 22 | 23 | if (!user) { 24 | return null; 25 | } 26 | 27 | const isValid = await argon.verify(user.passwordHash, password); 28 | 29 | if (!isValid) { 30 | return null; 31 | } 32 | return { 33 | id: user.id, 34 | username: user.username, 35 | }; 36 | }, 37 | credentials: { 38 | username: { label: 'Username', type: 'text' }, 39 | password: { label: 'Password', type: 'password' }, 40 | }, 41 | }), 42 | ], 43 | callbacks: { 44 | async session(params) { 45 | if (!params.token.sub) { 46 | return params.session; 47 | } 48 | 49 | const user = await prisma.user.findFirst({ 50 | where: { 51 | id: params.token.sub, 52 | }, 53 | }); 54 | 55 | if (!user) { 56 | return params.session; 57 | } 58 | params.session.user = user; 59 | 60 | return params.session; 61 | }, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /src/lib/bot.ts: -------------------------------------------------------------------------------- 1 | import { Telegraf } from 'telegraf'; 2 | import prisma from './prisma'; 3 | 4 | export function getUserBots(userId: string) { 5 | return prisma.bot.findMany({ 6 | where: { 7 | userId, 8 | }, 9 | }); 10 | } 11 | 12 | export async function getBotInstance() { 13 | const botData = await prisma.bot.findFirst(); 14 | if (!botData) { 15 | throw new Error('Bot not found'); 16 | } 17 | 18 | return new Telegraf(botData.token); 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/client.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from '@trpc/react-query'; 2 | import type { AppRouter } from '~server/index'; 3 | 4 | export const trpc = createTRPCReact({}); 5 | -------------------------------------------------------------------------------- /src/lib/helpers/UpdateAppTitle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEffect } from 'react'; 3 | import { useAppStore } from '~lib/store'; 4 | 5 | function UpdateAppTitle({ title }: { title: string }) { 6 | const { setAppTitle } = useAppStore(); 7 | 8 | useEffect(() => { 9 | setAppTitle(title); 10 | }, [setAppTitle, title]); 11 | 12 | return null; 13 | } 14 | 15 | export default UpdateAppTitle; 16 | -------------------------------------------------------------------------------- /src/lib/moderations/basic.ts: -------------------------------------------------------------------------------- 1 | import { ModerationSection } from './types'; 2 | 3 | export const BasicModeration: ModerationSection[] = [ 4 | { 5 | title: 'Administrative', 6 | subTitle: 'Administrative settings for the chat', 7 | inputs: [ 8 | { 9 | id: 'enable_admin_commands', 10 | type: 'checkbox', 11 | label: 'Enable admin commands', 12 | hint: 'This will allow admins to use commands like /kick, /ban, etc.', 13 | }, 14 | { 15 | id: 'disable_chat_message', 16 | type: 'checkbox', 17 | label: 'Disable chat messages', 18 | hint: 'This will disable all chat messages.', 19 | }, 20 | ], 21 | }, 22 | { 23 | title: 'Warning System', 24 | subTitle: 'Configure the warning system', 25 | inputs: [ 26 | { 27 | id: 'enable_warning_system', 28 | type: 'checkbox', 29 | label: 'Enable warning system', 30 | }, 31 | { 32 | id: 'warning_system_threshold', 33 | type: 'text', 34 | label: 'Warning threshold', 35 | hint: 'The amount of warnings a user can receive before being kicked or banned.', 36 | value: '3', 37 | }, 38 | ], 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /src/lib/moderations/filters.ts: -------------------------------------------------------------------------------- 1 | import { InputType } from '~lib/types'; 2 | import { ModerationSection } from './types'; 3 | 4 | export enum MessageType { 5 | Link = 'Link', 6 | Command = 'Command', 7 | File = 'File', 8 | Sticker = 'Sticker', 9 | Mention = 'Mention', 10 | Games = 'Games', 11 | VoiceMessage = 'Voice Message', 12 | VideoMessage = 'Video Message', 13 | AudioMessage = 'Audio Message', 14 | PhotoMessage = 'Photo Message', 15 | GIFMessage = 'GIF Message', 16 | AnimatedDice = 'Animated Dice', 17 | CustomEmoji = 'Custom Emoji', 18 | Stories = 'Stories', 19 | } 20 | 21 | export enum MessageFilter { 22 | Allow = 'Allow', 23 | Disallow = 'Disallow', 24 | } 25 | 26 | export const ExceptionInputs: Partial> = { 27 | Link: [ 28 | { 29 | id: 'filter_link_exception', 30 | type: 'text', 31 | label: 'Exception URLs', 32 | }, 33 | { 34 | id: 'filter_link_warn_on_violation', 35 | type: 'checkbox', 36 | label: 'Warn on violation', 37 | }, 38 | ], 39 | }; 40 | 41 | export const FilterModerations: ModerationSection[] = [ 42 | { 43 | title: 'Service Messages', 44 | subTitle: 'Select the service messages you want to filter', 45 | inputs: [ 46 | { 47 | id: 'delete_service_message_new_user', 48 | type: 'checkbox', 49 | label: 'Delete service messages about new users', 50 | }, 51 | { 52 | id: 'delete_service_message_leaving_user', 53 | type: 'checkbox', 54 | label: 'Delete service messages about leaving users', 55 | }, 56 | { 57 | id: 'delete_service_message_pinned_message', 58 | type: 'checkbox', 59 | label: 'Delete service messages about pinned messages', 60 | }, 61 | ], 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /src/lib/moderations/ids.ts: -------------------------------------------------------------------------------- 1 | import { InputType } from '~lib/types'; 2 | 3 | export const ModerationIDs = [ 4 | // Basic 5 | 'disable_chat_message', 6 | 'enable_admin_commands', 7 | 'enable_warning_system', 8 | 'warning_system_threshold', 9 | 10 | // New Users 11 | 'welcome_message', 12 | 'welcome_message_buttons', 13 | 14 | // Policies 15 | 'new_user_disable_message', 16 | 'new_user_disable_media', 17 | 'new_user_disable_link', 18 | 'new_user_disable_sticker', 19 | 'new_user_disable_invite', 20 | 21 | // Filters 22 | 'delete_service_message_new_user', 23 | 'delete_service_message_leaving_user', 24 | 'delete_service_message_pinned_message', 25 | ] as const; 26 | -------------------------------------------------------------------------------- /src/lib/moderations/new-users.ts: -------------------------------------------------------------------------------- 1 | import { ModerationSection } from './types'; 2 | 3 | export const NewUserModerations: ModerationSection[] = [ 4 | { 5 | title: 'Welcome Message', 6 | subTitle: 'Configure the welcome message and setup keyboard buttons', 7 | inputs: [ 8 | { 9 | id: 'welcome_message', 10 | type: 'markdown', 11 | label: 'Welcome message', 12 | hint: 'The message that will be sent to new users when they join.', 13 | }, 14 | { 15 | id: 'welcome_message_buttons', 16 | type: 'url_buttons', 17 | }, 18 | ], 19 | }, 20 | { 21 | title: 'Policies', 22 | subTitle: 'Apply policies to new users', 23 | inputs: [ 24 | { 25 | id: 'new_user_disable_message', 26 | type: 'checkbox', 27 | label: 'Disable message', 28 | }, 29 | { 30 | id: 'new_user_disable_media', 31 | type: 'checkbox', 32 | label: 'Disable media', 33 | }, 34 | { 35 | id: 'new_user_disable_link', 36 | type: 'checkbox', 37 | label: 'Disable link', 38 | }, 39 | { 40 | id: 'new_user_disable_sticker', 41 | type: 'checkbox', 42 | label: 'Disable sticker', 43 | }, 44 | { 45 | id: 'new_user_disable_invite', 46 | type: 'checkbox', 47 | label: 'Disable invite', 48 | }, 49 | ], 50 | }, 51 | ]; 52 | -------------------------------------------------------------------------------- /src/lib/moderations/types.ts: -------------------------------------------------------------------------------- 1 | import { InputType } from '~lib/types'; 2 | import { ModerationIDs } from './ids'; 3 | 4 | export type ModerationInputType = InputType<(typeof ModerationIDs)[number]>; 5 | 6 | export type ModerationSection = { 7 | title: string; 8 | subTitle: string; 9 | inputs: ModerationInputType[]; 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient({ 5 | log: ['warn'], 6 | }); 7 | }; 8 | 9 | type PrismaClientSingleton = ReturnType; 10 | 11 | const globalForPrisma = globalThis as unknown as { 12 | prisma: PrismaClientSingleton | undefined; 13 | }; 14 | 15 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); 16 | 17 | export default prisma; 18 | 19 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; 20 | -------------------------------------------------------------------------------- /src/lib/routers/announcements.ts: -------------------------------------------------------------------------------- 1 | import { protectedProcedure } from '~server/trpc'; 2 | import { z } from 'zod'; 3 | import { getBotInstance } from '~lib/bot'; 4 | 5 | export const createAnnouncement = protectedProcedure 6 | .input( 7 | z.object({ 8 | text: z.string(), 9 | chatId: z.string(), 10 | }) 11 | ) 12 | .mutation(async ({ input }) => { 13 | const bot = await getBotInstance(); 14 | 15 | try { 16 | await bot.telegram.sendMessage(input.chatId, input.text, { 17 | parse_mode: 'Markdown', 18 | }); 19 | } catch (e) { 20 | console.error(e); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/lib/routers/bots.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { Telegraf } from 'telegraf'; 3 | import { protectedProcedure } from '~server/trpc'; 4 | import prisma from '~lib/prisma'; 5 | 6 | // TODO: Automatic webhook address from Vercel environment variable 7 | 8 | // Use Vercel environment variable to determine if the app is running on Vercel 9 | // const IS_VERCEL = process.env.VERCEL === '1'; 10 | 11 | // Use Vercel environment variable to determine the webhook address 12 | const WEBHOOK_ADDRESS = // IS_VERCEL 13 | // ? `https://${process.env.VERCEL_URL}/api/webhook` 14 | // : process.env.WEBHOOK_ADDRESS 15 | process.env.WEBHOOK_ADDRESS as string; 16 | 17 | export const addBot = protectedProcedure 18 | .input( 19 | z.object({ 20 | token: z.string(), 21 | }) 22 | ) 23 | .mutation(async ({ input, ctx }) => { 24 | const { user } = ctx.session; 25 | const { token } = input; 26 | 27 | try { 28 | const bot = new Telegraf(token); 29 | 30 | const me = await bot.telegram.getMe(); 31 | 32 | const webhookAddress = new URL(WEBHOOK_ADDRESS); 33 | webhookAddress.searchParams.append('botId', me.id.toString()); 34 | await bot.telegram.setWebhook(webhookAddress.toString()); 35 | 36 | // todo: error handling 37 | 38 | try { 39 | return await prisma.bot.create({ 40 | data: { 41 | id: me.id.toString(), 42 | name: me.first_name, 43 | username: me.username, 44 | userId: user.id, 45 | botInfo: me as any, 46 | token, 47 | }, 48 | }); 49 | } catch (error) { 50 | console.error(error); 51 | return null; 52 | } 53 | } catch (e) { 54 | console.error(e); 55 | return null; 56 | } 57 | }); 58 | 59 | export const restartBot = protectedProcedure 60 | .input( 61 | z.object({ 62 | botId: z.string(), 63 | }) 64 | ) 65 | .mutation(async ({ input }) => { 66 | const { botId } = input; 67 | const webhookAddress = new URL(WEBHOOK_ADDRESS); 68 | 69 | try { 70 | const botData = await prisma.bot.findUnique({ 71 | where: { 72 | id: botId, 73 | }, 74 | }); 75 | 76 | if (!botData) { 77 | throw new Error('Bot not found'); 78 | } 79 | 80 | const bot = new Telegraf(botData.token); 81 | 82 | await bot.telegram.deleteWebhook(); 83 | 84 | webhookAddress.searchParams.append('botId', botId); 85 | await bot.telegram.setWebhook(webhookAddress.toString()); 86 | 87 | return true; 88 | } catch (error) { 89 | console.error(error); 90 | return false; 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /src/lib/routers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ping'; 2 | export * from './announcements'; 3 | export * from './bots'; 4 | -------------------------------------------------------------------------------- /src/lib/routers/ping.ts: -------------------------------------------------------------------------------- 1 | import { publicProcedure } from '~server/trpc'; 2 | 3 | export const ping = publicProcedure.query(() => { 4 | return 'pong'; 5 | }); 6 | -------------------------------------------------------------------------------- /src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | type AppStore = { 4 | appTitle: string; 5 | setAppTitle: (title: string) => void; 6 | }; 7 | export const useAppStore = create((set) => ({ 8 | appTitle: '', 9 | setAppTitle: (title) => set({ appTitle: title }), 10 | })); 11 | 12 | // Moderation Store 13 | 14 | type ModerationStore = {}; 15 | export const useModerationStore = create((set) => ({})); 16 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type InputType = { 2 | id: T; 3 | type: 'text' | 'checkbox' | 'select' | 'markdown' | 'url_buttons'; 4 | label?: string; 5 | hint?: string; 6 | value?: string | boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/server/context.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from '~lib/auth'; 2 | import prisma from '~lib/prisma'; 3 | import trpc from '@trpc/server'; 4 | import { Session, getServerSession } from 'next-auth'; 5 | 6 | export async function createContext() { 7 | const session = (await getServerSession(authOptions)) as Session; 8 | 9 | return { 10 | prisma, 11 | session, 12 | }; 13 | } 14 | 15 | export type Context = trpc.inferAsyncReturnType; 16 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { router } from './trpc'; 2 | import * as routers from '~lib/routers'; 3 | 4 | export const appRouter = router(routers); 5 | 6 | export type AppRouter = typeof appRouter; 7 | -------------------------------------------------------------------------------- /src/server/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server'; 2 | import { Context } from './context'; 3 | 4 | const t = initTRPC.context().create(); 5 | 6 | const isAuthed = t.middleware((opts) => { 7 | const { ctx } = opts; 8 | 9 | if (!ctx.session) { 10 | throw new Error('Not authorized'); 11 | } 12 | 13 | return opts.next({ 14 | ctx, 15 | }); 16 | }); 17 | 18 | export const router = t.router; 19 | export const publicProcedure = t.procedure; 20 | export const protectedProcedure = t.procedure.use(isAuthed); 21 | -------------------------------------------------------------------------------- /src/telegram/bot.ts: -------------------------------------------------------------------------------- 1 | import { Composer } from 'telegraf'; 2 | import databaseComposer from './database'; 3 | 4 | const bot = new Composer(); 5 | 6 | bot.use(databaseComposer); 7 | 8 | bot.command('start', (ctx) => { 9 | ctx.reply('Hello!'); 10 | }); 11 | 12 | export default bot; 13 | -------------------------------------------------------------------------------- /src/telegram/database.ts: -------------------------------------------------------------------------------- 1 | import { Composer } from 'telegraf'; 2 | import { ChatMemberOwner, ChatMemberAdministrator } from 'telegraf/types'; 3 | import prisma from '~lib/prisma'; 4 | 5 | const databaseComposer = new Composer(); 6 | 7 | function convertAdminsToJSON( 8 | input: (ChatMemberOwner | ChatMemberAdministrator)[] 9 | ) { 10 | const final: Record = {}; 11 | 12 | for (const admin of input) { 13 | if (admin.user.id) { 14 | final[admin.user.id] = admin; 15 | } 16 | } 17 | 18 | return final; 19 | } 20 | 21 | function handleCreateChat(chat: any) { 22 | return prisma.telegramChat.create({ 23 | data: chat, 24 | }); 25 | } 26 | 27 | databaseComposer.use(async (ctx, next) => { 28 | next(); 29 | 30 | const chat = ctx.chat; 31 | 32 | if (ctx.myChatMember) { 33 | const { new_chat_member } = ctx.myChatMember; 34 | 35 | // Only run if the bot is added to a group/channel 36 | if (new_chat_member.user.id === ctx.botInfo.id) { 37 | if (new_chat_member.status === 'administrator') { 38 | if (chat) { 39 | const chatExist = await prisma.telegramChat.findUnique({ 40 | where: { 41 | id: chat.id.toString(), 42 | }, 43 | }); 44 | 45 | if (chatExist) { 46 | return; 47 | } 48 | 49 | const toalMembers = await ctx.getChatMembersCount(); 50 | 51 | if ( 52 | chat.type === 'supergroup' || 53 | chat.type === 'group' || 54 | chat.type === 'channel' 55 | ) { 56 | const admins = await ctx.getChatAdministrators(); 57 | await prisma.telegramChat.create({ 58 | data: { 59 | id: chat.id.toString(), 60 | type: chat.type, 61 | title: chat.title, 62 | admins: convertAdminsToJSON(admins) as never, 63 | totalMembers: toalMembers, 64 | }, 65 | }); 66 | } 67 | } 68 | } 69 | return; 70 | } 71 | } 72 | }); 73 | 74 | databaseComposer.on('message', async (ctx, next) => { 75 | next(); 76 | 77 | const user = ctx.from; 78 | const chat = ctx.chat; 79 | 80 | if (!chat) return; 81 | 82 | try { 83 | if (user) { 84 | const userExist = await prisma.telegramUser.findFirst({ 85 | where: { 86 | id: user.id.toString(), 87 | }, 88 | }); 89 | 90 | if (!userExist) { 91 | await prisma.telegramUser.create({ 92 | data: { 93 | id: user.id.toString(), 94 | firstName: user.first_name, 95 | lastName: user.last_name, 96 | username: user.username, 97 | }, 98 | }); 99 | } 100 | 101 | const totalChats = await prisma.telegramChat.count({ 102 | where: { 103 | id: chat.id.toString(), 104 | }, 105 | }); 106 | 107 | if (totalChats === 0) { 108 | const toalMembers = await ctx.getChatMembersCount(); 109 | 110 | if (chat.type === 'supergroup' || chat.type === 'group') { 111 | const admins = await ctx.getChatAdministrators(); 112 | await prisma.telegramChat.create({ 113 | data: { 114 | id: chat.id.toString(), 115 | type: chat.type, 116 | title: chat.title, 117 | admins: convertAdminsToJSON(admins) as never, 118 | totalMembers: toalMembers, 119 | }, 120 | }); 121 | } 122 | } 123 | 124 | const userChatExist = await prisma.telegramUserChat.findFirst({ 125 | where: { 126 | chatId: chat.id.toString(), 127 | userId: user.id.toString(), 128 | }, 129 | }); 130 | 131 | if (!userChatExist) { 132 | await prisma.telegramUserChat.create({ 133 | data: { 134 | chatId: chat.id.toString(), 135 | userId: user.id.toString(), 136 | }, 137 | }); 138 | } 139 | } 140 | } catch (e) { 141 | console.error(e); 142 | } 143 | }); 144 | 145 | export default databaseComposer; 146 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { nextui } from '@nextui-org/react'; 2 | import type { Config } from 'tailwindcss'; 3 | 4 | const config = { 5 | content: [ 6 | './src/**/*.{ts,tsx}', 7 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', 8 | ], 9 | prefix: '', 10 | theme: {}, 11 | darkMode: ['class'], 12 | plugins: [ 13 | require('tailwindcss-animate'), 14 | nextui({ 15 | themes: { 16 | light: { 17 | colors: { 18 | primary: { 19 | DEFAULT: '#1c1c1c', 20 | }, 21 | }, 22 | }, 23 | }, 24 | }), 25 | ], 26 | } satisfies Config; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "~lib/*": ["./src/lib/*"], 22 | "~components/*": ["./src/components/*"], 23 | "~assets/*": ["./src/assets/*"], 24 | "~server/*": ["./src/server/*"], 25 | "~telegram/*": ["./src/telegram/*"] 26 | } 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | --------------------------------------------------------------------------------