├── app ├── login.css ├── types │ ├── index.d.ts │ ├── Cookie.d.ts │ ├── User.d.ts │ ├── Database.d.ts │ ├── FancyArea.d.ts │ └── Thread.d.ts ├── lib │ ├── utils.ts │ └── getFIleTypeIcon.tsx ├── .server │ ├── redis.ts │ ├── cookies.ts │ ├── utils.ts │ ├── sessions.ts │ └── db.server.ts ├── entry.client.tsx ├── routes │ ├── api.threads.list.tsx │ ├── auth.verify.tsx │ ├── files.$filename.tsx │ ├── auth.register.tsx │ ├── _index.tsx │ ├── threads.$id.tsx │ ├── auth.login.tsx │ ├── register.tsx │ ├── login.tsx │ ├── dashboard.tsx │ └── threads.new.tsx ├── components │ ├── ui │ │ ├── textarea.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── badge.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── tabs.tsx │ │ ├── card.tsx │ │ ├── table.tsx │ │ ├── sheet.tsx │ │ ├── alert-dialog.tsx │ │ ├── navbar.tsx │ │ └── dropdown-menu.tsx │ ├── alert-dialog-json.tsx │ └── fancy-area.tsx ├── root.tsx ├── tailwind.css └── entry.server.tsx ├── db ├── index.ts └── schema │ ├── tokens.ts │ ├── refreshTokens.ts │ ├── users.ts │ └── threads.ts ├── public └── favicon.ico ├── postcss.config.js ├── .gitignore ├── remix.config.js ├── drizzle.config.ts ├── drizzle └── meta │ ├── _journal.json │ └── 0000_snapshot.json ├── components.json ├── .tailwind.config.ts ├── vite.config.ts ├── .github └── workflows │ └── lint.yml ├── .dummyacc.schema.json ├── tsconfig.json ├── README.md ├── .eslintrc.cjs ├── tailwind.config.ts └── package.json /app/login.css: -------------------------------------------------------------------------------- 1 | .main { 2 | background-color: rgb(37, 99, 235); 3 | } -------------------------------------------------------------------------------- /db/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./schema/users"; 2 | export * from "./schema/tokens"; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrup/RucoTalk/master/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./User"; 2 | export * from "./Cookie"; 3 | export * from "./FancyArea"; 4 | export * from "./Database"; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | .dummyacc.json 7 | remix-* 8 | uploads 9 | drizzle 10 | .vercel 11 | public/build -------------------------------------------------------------------------------- /app/types/Cookie.d.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./User"; 2 | 3 | export interface LoginCookie { 4 | token: string; 5 | user: User; 6 | isLoggedIn: boolean; 7 | } -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | export default { 3 | tailwind: true, 4 | postcss: true, 5 | browserNodeBuiltinsPolyfill: { modules: { events: true } }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | export default defineConfig({ 4 | dialect: "postgresql", 5 | schema: "./db/schema/*.ts", 6 | out: "./drizzle", 7 | dbCredentials: { 8 | url: process.env.DATABASE_URL!, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1725457852320, 9 | "tag": "0000_sleepy_blink", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /app/types/User.d.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | username: string | null; 4 | email: string | null; 5 | password: string; 6 | roles: UserRole[] | null; 7 | createdAt: Date | null; 8 | updatedAt: Date | null; 9 | } 10 | 11 | export type UserRole = "admin" | "user" | "moderator"; 12 | -------------------------------------------------------------------------------- /app/types/Database.d.ts: -------------------------------------------------------------------------------- 1 | import { users as usersDb, tokens as tokensDb } from "db"; 2 | 3 | export interface User { 4 | username: string; 5 | email: string; 6 | password: string; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | } 10 | 11 | export enum Tables { 12 | users = usersDb, 13 | tokens = tokensDb, 14 | } 15 | -------------------------------------------------------------------------------- /app/types/FancyArea.d.ts: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from "react"; 2 | 3 | export interface FancyAreaProps { 4 | textAreaName?: string; 5 | textAreaId?: string; 6 | textAreaClassName?: string; 7 | textAreaValue?: string; 8 | textAreaReadOnly?: boolean; 9 | textAreaOnChange?: (e: any) => void; 10 | textAreaPlaceholder?: string; 11 | } 12 | -------------------------------------------------------------------------------- /app/types/Thread.d.ts: -------------------------------------------------------------------------------- 1 | export interface Thread { 2 | id: string; 3 | title: string; 4 | content: string; 5 | authorId: string; 6 | attachments: Attachment[]; 7 | status: "open" | "closed"; 8 | createdAt?: Date; 9 | updatedAt?: Date; 10 | } 11 | 12 | export interface Attachment { 13 | name: string; 14 | url: string; 15 | type: string; 16 | } 17 | -------------------------------------------------------------------------------- /app/.server/redis.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "redis"; 2 | 3 | const client = createClient({ 4 | password: "rww09binPrxgBfbJSWdP61vTJCYOnMea", 5 | socket: { 6 | host: "redis-16932.c85.us-east-1-2.ec2.redns.redis-cloud.com", 7 | port: 16932, 8 | }, 9 | }); 10 | 11 | client.on("error", (err) => console.log("Redis Client Error", err)); 12 | 13 | export { client }; 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/tailwind.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], 5 | theme: { 6 | extend: { 7 | colors: { 8 | navy: { 9 | 900: "#1e2a4a", // Adjust this value to match the exact shade in the image 10 | }, 11 | }, 12 | }, 13 | }, 14 | plugins: [require("@tailwindcss/forms")], 15 | } satisfies Config; 16 | -------------------------------------------------------------------------------- /db/schema/tokens.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; 2 | import { users } from "./users"; 3 | 4 | export const tokens = pgTable("tokens", { 5 | id: uuid("id").primaryKey().defaultRandom().notNull(), 6 | token: varchar("token").notNull().unique(), 7 | user_id: uuid("user_id") 8 | .notNull() 9 | .references(() => users.id), 10 | created_at: timestamp("createdAt").defaultNow(), 11 | updated_at: timestamp("updatedAt").defaultNow(), 12 | }); 13 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { vitePlugin as remix } from "@remix-run/dev"; 2 | import { defineConfig } from "vite"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | remix({ 8 | future: { 9 | v3_fetcherPersist: true, 10 | v3_relativeSplatPath: true, 11 | v3_throwAbortReason: true, 12 | }, 13 | }), 14 | tsconfigPaths(), 15 | ], 16 | optimizeDeps: { 17 | exclude: ["@mapbox"], 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: "14" 17 | 18 | - name: Install dependencies 19 | run: npm install 20 | 21 | - name: Run ESLint 22 | run: npx eslint . --ext .js,.jsx,.ts,.tsx 23 | -------------------------------------------------------------------------------- /db/schema/refreshTokens.ts: -------------------------------------------------------------------------------- 1 | import { jsonb, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; 2 | import { users } from "./users"; 3 | 4 | export const tokens = pgTable("RefreshToken", { 5 | id: uuid("id").primaryKey().defaultRandom().notNull(), 6 | hashedToken: varchar("hashedToken").notNull().unique(), 7 | userId: uuid("user_id") 8 | .notNull() 9 | .references(() => users.id), 10 | createdAt: timestamp("createdAt").defaultNow(), 11 | updatedAt: timestamp("updatedAt").defaultNow(), 12 | }); 13 | -------------------------------------------------------------------------------- /app/.server/cookies.ts: -------------------------------------------------------------------------------- 1 | import { createCookie } from "@remix-run/node"; 2 | 3 | const oneweek = 60 * 60 * 24 * 7; // 1 week 4 | 5 | export const login = createCookie("__login", { 6 | secrets: [process.env.LOGIN_SECRET!], // haha 7 | maxAge: oneweek, 8 | }); 9 | 10 | const sesionExpires = new Date(); 11 | sesionExpires.setSeconds(sesionExpires.getSeconds() + oneweek); 12 | 13 | export const sessionCookie = createCookie("__session", { 14 | secrets: [process.env.SESSION_SECRET!], // haha i changed it lol 15 | sameSite: true, 16 | // expires: sesionExpires, 17 | }); 18 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from "@remix-run/react"; 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /db/schema/users.ts: -------------------------------------------------------------------------------- 1 | import { varchar, pgTable, uuid, timestamp } from "drizzle-orm/pg-core"; 2 | import crypto from "crypto"; 3 | 4 | function randomString(length: number) { 5 | return crypto.randomBytes(length).toString("hex"); 6 | } 7 | 8 | export const users = pgTable("users", { 9 | id: uuid("id").primaryKey().defaultRandom(), 10 | email: varchar("email").unique(), 11 | password: varchar("password").notNull(), 12 | username: varchar("username").default("Anonymous"), 13 | roles: varchar("roles").array().default(["user"]), 14 | createdAt: timestamp("createdAt").defaultNow(), 15 | updatedAt: timestamp("updatedAt").defaultNow(), 16 | }); 17 | -------------------------------------------------------------------------------- /app/routes/api.threads.list.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunctionArgs } from "@remix-run/node"; 2 | import { threads } from "db/schema/threads"; 3 | import { DB } from "~/.server/db.server"; 4 | 5 | export async function loader({ request }: LoaderFunctionArgs) { 6 | const db = new DB(); 7 | switch (request.method) { 8 | case "GET": 9 | const threadsList = await db.db.select().from(threads); 10 | 11 | if ( 12 | request.headers.get("Authorization") !== 13 | `Bearer ${process.env.REFRESH_SECRET}` 14 | ) { 15 | return json({ message: "Unauthorized" }, 401); 16 | } 17 | 18 | return json(threadsList, 200); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /db/schema/threads.ts: -------------------------------------------------------------------------------- 1 | import { 2 | varchar, 3 | text, 4 | timestamp, 5 | pgTable, 6 | uuid, 7 | jsonb, 8 | } from "drizzle-orm/pg-core"; 9 | import { users } from "./users"; 10 | 11 | export const threads = pgTable("threads", { 12 | id: uuid("id").primaryKey().defaultRandom(), 13 | title: varchar("title").notNull(), 14 | content: text("content").notNull(), 15 | authorId: uuid("author_id") 16 | .notNull() 17 | .references(() => users.id), 18 | status: varchar("status", { enum: ["open", "closed"] }).default("open"), 19 | attachments: jsonb("attachments").array().default([]), 20 | createdAt: timestamp("createdAt").defaultNow(), 21 | updatedAt: timestamp("updatedAt").defaultNow(), 22 | }); 23 | -------------------------------------------------------------------------------- /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 |