├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── README.md
├── bun.lockb
├── challenge.md
├── components.json
├── docker-compose.yml
├── index.html
├── package.json
├── postcss.config.js
├── public
└── vite.svg
├── src
├── app.tsx
├── components
│ ├── chat
│ │ ├── chat-container.tsx
│ │ ├── index.ts
│ │ ├── message-bubble-skeleton.tsx
│ │ ├── message-bubble.tsx
│ │ ├── message-list-skeleton.tsx
│ │ └── message-list.tsx
│ └── ui
│ │ ├── button.tsx
│ │ └── skeleton.tsx
├── data-access
│ └── use-messages-query.ts
├── index.css
├── lib
│ ├── app-runtime.ts
│ ├── query-client.ts
│ └── utils.ts
├── main.tsx
├── types
│ └── message.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:react-hooks/recommended",
8 | ],
9 | ignorePatterns: ["dist", ".eslintrc.cjs"],
10 | parser: "@typescript-eslint/parser",
11 | plugins: ["react-refresh"],
12 | rules: {
13 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
14 | "@typescript-eslint/no-namespace": "off",
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-tailwindcss"],
3 | "singleQuote": false,
4 | "trailingComma": "all",
5 | "arrowParens": "always",
6 | "endOfLine": "lf",
7 | "printWidth": 100,
8 | "tabWidth": 2,
9 | "useTabs": false,
10 | "semi": true,
11 | "bracketSpacing": true
12 | }
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default {
18 | // other rules...
19 | parserOptions: {
20 | ecmaVersion: 'latest',
21 | sourceType: 'module',
22 | project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'],
23 | tsconfigRootDir: __dirname,
24 | },
25 | }
26 | ```
27 |
28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
31 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucas-barake/react-interview-test-queues-and-streams/b5962fec3b6199539b9bd3d7de7da93b6cd77411/bun.lockb
--------------------------------------------------------------------------------
/challenge.md:
--------------------------------------------------------------------------------
1 | ## The Problem
2 |
3 | You're building a chat application similar to Slack or Discord. While messages have a `readAt: DateTime | null` property, the front-end needs to mark them as read.
4 |
5 | ## Context
6 |
7 | The application displays messages in a scrollable feed. When a message becomes visible and the user has actively viewed it (i.e., the document is in focus), we need to mark it as read on the server. However, sending individual requests for each message would overwhelm the server.
8 |
9 | ## Core Requirements
10 |
11 | Your solution should implement a read status tracking system that batches updates based on two triggers: either reaching 25 messages or a 5-second time window, whichever comes first. The system should handle network failures gracefully and ensure no messages are incorrectly marked as read.
12 |
13 | As messages scroll into view while the document is in focus, the UI should optimistically mark them as read. These status updates need to be batched and sent to the server efficiently.
14 |
15 | ## Technical Challenges
16 |
17 | The system must be resilient to network failures. When a batch update fails, implement a retry mechanism with exponential backoff, starting at 500ms and doubling with each attempt, up to 3 retries. During network outages, failed batches should be queued and automatically retried when connectivity resumes.
18 |
19 | If a batch fails all retry attempts, the system should reset the read status of affected messages and make them eligible for future batching.
20 |
21 | ## Testing Scenarios
22 |
23 | Your solution will be tested under various conditions:
24 |
25 | - Rapid scrolling through many messages
26 | - Browser tab switching while messages are being processed
27 | - Network disconnections during batch processing
28 | - Recovery after extended offline periods
29 |
30 | ## Time Limit
31 |
32 | You have 1 hour to implement this solution. Focus on building a robust system that handles edge cases gracefully while maintaining clean, maintainable code.
33 |
34 | ## Evaluation Criteria
35 |
36 | Your solution will be evaluated on its resilience to network issues, proper use of React patterns, memory management, and overall code organization. We're particularly interested in how you handle race conditions and edge cases.
37 |
--------------------------------------------------------------------------------
/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": "src/index.css",
9 | "baseColor": "stone",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | jaeger:
3 | container_name: vite-project-jaeger
4 | image: jaegertracing/all-in-one:latest
5 | command:
6 | - "--collector.otlp.http.cors.allowed-origins=http://localhost:5173"
7 | - "--collector.otlp.http.cors.allowed-headers=*"
8 | - "--collector.otlp.http.tls.enabled=false"
9 | ports:
10 | - "6831:6831/udp"
11 | - "6832:6832/udp"
12 | - "5778:5778"
13 | - "16686:16686"
14 | - "4317:4317"
15 | - "4318:4318"
16 | - "14250:14250"
17 | - "14268:14268"
18 | - "14269:14269"
19 | - "9411:9411"
20 | environment:
21 | - COLLECTOR_OTLP_ENABLED=true
22 | - QUERY_BASE_PATH=/
23 | networks:
24 | - jaeger-network
25 | restart: no
26 |
27 | networks:
28 | jaeger-network:
29 | driver: bridge
30 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@effect/opentelemetry": "^0.35.0",
14 | "@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
15 | "@opentelemetry/sdk-trace-web": "^1.25.1",
16 | "@radix-ui/react-slot": "^1.1.0",
17 | "@tanstack/react-query": "^5.59.20",
18 | "@tanstack/react-query-devtools": "^5.59.20",
19 | "class-variance-authority": "^0.7.0",
20 | "clsx": "^2.1.1",
21 | "effect": "^3.10.14",
22 | "lucide-react": "^0.456.0",
23 | "react": "^18.3.1",
24 | "react-dom": "^18.3.1",
25 | "tailwind-merge": "^2.5.4",
26 | "tailwindcss-animate": "^1.0.7"
27 | },
28 | "devDependencies": {
29 | "@types/node": "^22.9.0",
30 | "@types/react": "^18.3.3",
31 | "@types/react-dom": "^18.3.0",
32 | "@typescript-eslint/eslint-plugin": "^7.15.0",
33 | "@typescript-eslint/parser": "^7.15.0",
34 | "@vitejs/plugin-react": "^4.3.3",
35 | "@vitejs/plugin-react-swc": "^3.5.0",
36 | "autoprefixer": "^10.4.20",
37 | "eslint": "^8.57.0",
38 | "eslint-plugin-react-hooks": "^4.6.2",
39 | "eslint-plugin-react-refresh": "^0.4.7",
40 | "postcss": "^8.4.49",
41 | "prettier": "^3.3.3",
42 | "prettier-plugin-tailwindcss": "^0.6.5",
43 | "tailwindcss": "^3.4.14",
44 | "typescript": "^5.2.2",
45 | "vite": "^5.3.4"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { ChatContainer } from "./components/chat";
2 |
3 | function App() {
4 | return (
5 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/src/components/chat/chat-container.tsx:
--------------------------------------------------------------------------------
1 | import { MessagesQuery } from "@/data-access/use-messages-query";
2 | import { MessageList } from "./message-list";
3 | import { MessageListSkeleton } from "./message-list-skeleton";
4 | import { Button } from "@/components/ui/button";
5 | import { AlertCircle } from "lucide-react";
6 | import { UseQueryResult } from "@tanstack/react-query";
7 | import { Message } from "../../types/message";
8 |
9 | const ErrorState: React.FC<{ messagesQuery: UseQueryResult }> = ({ messagesQuery }) => {
10 | return (
11 |
12 |
13 |
14 |
Error loading messages
15 |
Something went wrong. Please try again.
16 |
17 |
21 |
22 | );
23 | };
24 |
25 | export const ChatContainer: React.FC = () => {
26 | const messagesQuery = MessagesQuery.useMessagesQuery();
27 |
28 | return (
29 |
30 |
31 |
Messages
32 |
33 |
34 |
35 | {messagesQuery.isLoading ? (
36 |
37 | ) : !messagesQuery.isSuccess ? (
38 |
39 | ) : (
40 |
41 | )}
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/components/chat/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./chat-container";
2 | export * from "./message-bubble";
3 | export * from "./message-list";
4 |
--------------------------------------------------------------------------------
/src/components/chat/message-bubble-skeleton.tsx:
--------------------------------------------------------------------------------
1 | interface MessageBubbleSkeletonProps {
2 | width?: number;
3 | }
4 |
5 | export const MessageBubbleSkeleton: React.FC = ({ width = 200 }) => {
6 | return (
7 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/chat/message-bubble.tsx:
--------------------------------------------------------------------------------
1 | import { Message } from "@/types/message";
2 | import { CheckCheckIcon } from "lucide-react";
3 | import React from "react";
4 | import { cn } from "@/lib/utils";
5 | import { DateTime } from "effect";
6 |
7 | type Props = {
8 | message: Message;
9 | };
10 |
11 | export const MessageBubble = React.forwardRef<
12 | HTMLDivElement,
13 | Props & React.HTMLAttributes
14 | >(({ message, ...props }, ref) => {
15 | return (
16 |
17 |
18 |
{message.body}
19 |
20 |
21 |
22 | {message.createdAt.pipe(
23 | DateTime.format({
24 | hour: "2-digit",
25 | minute: "2-digit",
26 | }),
27 | )}
28 |
29 |
30 |
33 |
34 |
35 |
36 | );
37 | });
38 |
39 | MessageBubble.displayName = "MessageBubble";
40 |
--------------------------------------------------------------------------------
/src/components/chat/message-list-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { MessageBubbleSkeleton } from "./message-bubble-skeleton";
2 |
3 | export const MessageListSkeleton: React.FC = () => {
4 | const widths = [180, 260, 200, 180, 260, 200, 180, 260, 200];
5 |
6 | return (
7 |
8 | {widths.map((width, i) => (
9 |
10 | ))}
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/chat/message-list.tsx:
--------------------------------------------------------------------------------
1 | import { Message } from "@/types/message";
2 | import { MessageBubble } from "./message-bubble";
3 |
4 | type Props = {
5 | messages: Message[];
6 | };
7 |
8 | export const MessageList: React.FC = ({ messages }) => {
9 | return (
10 |
11 | {messages.map((message) => (
12 |
13 | ))}
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/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 gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/data-access/use-messages-query.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { Message, MessageId } from "../types/message";
3 | import { Array, DateTime, Effect, Random } from "effect";
4 | import { queryClient, Updater } from "../lib/query-client";
5 |
6 | const sampleMessageBodies = [
7 | "Hey there! How are you doing today?",
8 | "I'm doing great, thanks for asking! How about you?",
9 | "Pretty good! Just finished my morning coffee.",
10 | "Nice! I'm still working on mine. Did you see the weather forecast?",
11 | "Yeah, looks like rain later today.",
12 | "Perfect weather for staying in and coding!",
13 | "Absolutely! What are you working on these days?",
14 | "Building a chat application with React and TypeScript",
15 | "That sounds interesting! How's it going so far?",
16 | "Pretty well! Just working on the UI components now.",
17 | "Are you using any UI libraries?",
18 | "Yeah, I'm using Tailwind CSS for styling",
19 | "Nice choice! I love Tailwind's utility-first approach",
20 | "Me too! It makes styling so much faster",
21 | "Have you tried any component libraries with it?",
22 | "I've been looking at shadcn/ui actually",
23 | "That's a great choice! Very customizable",
24 | "Yeah, I like how it's not a dependency",
25 | "Are you planning to add any real-time features?",
26 | "Definitely! Thinking about using WebSocket",
27 | "Have you worked with WebSocket before?",
28 | "A little bit, but I'm excited to learn more",
29 | "That's the best way to learn - by doing!",
30 | "Exactly! It's been fun so far",
31 | "Oh, looks like it started raining",
32 | "Perfect timing for coding, just like we said!",
33 | "Absolutely! Time to grab another coffee",
34 | "Good idea! I should do the same",
35 | "Talk to you later then?",
36 | "Definitely! Enjoy your coffee!",
37 | ];
38 |
39 | const sampleMessages = Array.makeBy(30, (i) => ({
40 | id: MessageId.make(`${i + 1}`),
41 | body: sampleMessageBodies[i],
42 | createdAt: DateTime.unsafeMake("2024-03-20T10:00:00Z").pipe(
43 | DateTime.add({
44 | minutes: i * 2,
45 | }),
46 | ),
47 | readAt: null,
48 | }));
49 |
50 | export namespace MessagesQuery {
51 | const makeQueryKey = () => ["messages"] as const;
52 |
53 | export const useMessagesQuery = () => {
54 | return useQuery({
55 | queryKey: makeQueryKey(),
56 | queryFn: () =>
57 | Effect.gen(function* () {
58 | const sleepFor = yield* Random.nextRange(1_000, 2_500);
59 |
60 | yield* Effect.sleep(`${sleepFor} millis`);
61 |
62 | return sampleMessages;
63 | }).pipe(Effect.runPromise),
64 | });
65 | };
66 |
67 | export const setQueryData = (updater: Updater) =>
68 | queryClient.setQueryData(makeQueryKey(), updater);
69 | }
70 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | @layer base {
5 | :root {
6 | --background: 20 14.3% 4.1%;
7 | --foreground: 60 9.1% 97.8%;
8 | --card: 20 14.3% 4.1%;
9 | --card-foreground: 60 9.1% 97.8%;
10 | --popover: 20 14.3% 4.1%;
11 | --popover-foreground: 60 9.1% 97.8%;
12 | --primary: 60 9.1% 97.8%;
13 | --primary-foreground: 24 9.8% 10%;
14 | --secondary: 12 6.5% 15.1%;
15 | --secondary-foreground: 60 9.1% 97.8%;
16 | --muted: 12 6.5% 15.1%;
17 | --muted-foreground: 24 5.4% 63.9%;
18 | --accent: 12 6.5% 15.1%;
19 | --accent-foreground: 60 9.1% 97.8%;
20 | --destructive: 0 62.8% 30.6%;
21 | --destructive-foreground: 60 9.1% 97.8%;
22 | --border: 12 6.5% 15.1%;
23 | --input: 12 6.5% 15.1%;
24 | --ring: 24 5.7% 82.9%;
25 | --chart-1: 220 70% 50%;
26 | --chart-2: 160 60% 45%;
27 | --chart-3: 30 80% 55%;
28 | --chart-4: 280 65% 60%;
29 | --chart-5: 340 75% 55%;
30 | }
31 | .dark {
32 | --background: 20 14.3% 4.1%;
33 | --foreground: 60 9.1% 97.8%;
34 | --card: 20 14.3% 4.1%;
35 | --card-foreground: 60 9.1% 97.8%;
36 | --popover: 20 14.3% 4.1%;
37 | --popover-foreground: 60 9.1% 97.8%;
38 | --primary: 60 9.1% 97.8%;
39 | --primary-foreground: 24 9.8% 10%;
40 | --secondary: 12 6.5% 15.1%;
41 | --secondary-foreground: 60 9.1% 97.8%;
42 | --muted: 12 6.5% 15.1%;
43 | --muted-foreground: 24 5.4% 63.9%;
44 | --accent: 12 6.5% 15.1%;
45 | --accent-foreground: 60 9.1% 97.8%;
46 | --destructive: 0 62.8% 30.6%;
47 | --destructive-foreground: 60 9.1% 97.8%;
48 | --border: 12 6.5% 15.1%;
49 | --input: 12 6.5% 15.1%;
50 | --ring: 24 5.7% 82.9%;
51 | --chart-1: 220 70% 50%;
52 | --chart-2: 160 60% 45%;
53 | --chart-3: 30 80% 55%;
54 | --chart-4: 280 65% 60%;
55 | --chart-5: 340 75% 55%;
56 | }
57 | }
58 | @layer base {
59 | * {
60 | @apply border-border;
61 | }
62 | body {
63 | @apply bg-background text-foreground;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/lib/app-runtime.ts:
--------------------------------------------------------------------------------
1 | import { Layer, Logger, ManagedRuntime } from "effect";
2 | import { NetworkMonitor } from "@/lib/services/network-monitor.ts";
3 |
4 | export const AppRuntime = ManagedRuntime.make(
5 | Layer.mergeAll(Logger.pretty, NetworkMonitor.Default),
6 | );
7 |
--------------------------------------------------------------------------------
/src/lib/query-client.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from "@tanstack/react-query";
2 | import { Duration } from "effect";
3 |
4 | export type Updater = T | ((cachedData: T) => T);
5 |
6 | export const queryClient = new QueryClient({
7 | defaultOptions: {
8 | queries: {
9 | staleTime: Duration.toMillis("1 minute"),
10 | retry: false,
11 | refetchOnWindowFocus: false,
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: Parameters) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { QueryClientProvider } from "@tanstack/react-query";
4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5 | import { queryClient } from "./lib/query-client";
6 | import App from "./app";
7 | import "./index.css";
8 |
9 | ReactDOM.createRoot(document.getElementById("root")!).render(
10 |
11 |
12 |
13 |
14 |
15 | ,
16 | );
17 |
--------------------------------------------------------------------------------
/src/types/message.ts:
--------------------------------------------------------------------------------
1 | import { Schema } from "effect";
2 |
3 | export const MessageId = Schema.String.pipe(Schema.brand("MessageId"));
4 |
5 | export const Message = Schema.Struct({
6 | id: MessageId,
7 | body: Schema.String,
8 | createdAt: Schema.DateTimeUtc,
9 | readAt: Schema.NullOr(Schema.DateTimeUtc),
10 | });
11 |
12 | export type Message = typeof Message.Type;
13 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | darkMode: ["class"],
4 | content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
5 | theme: {
6 | extend: {
7 | borderRadius: {
8 | lg: "var(--radius)",
9 | md: "calc(var(--radius) - 2px)",
10 | sm: "calc(var(--radius) - 4px)",
11 | },
12 | colors: {
13 | background: "hsl(var(--background))",
14 | foreground: "hsl(var(--foreground))",
15 | card: {
16 | DEFAULT: "hsl(var(--card))",
17 | foreground: "hsl(var(--card-foreground))",
18 | },
19 | popover: {
20 | DEFAULT: "hsl(var(--popover))",
21 | foreground: "hsl(var(--popover-foreground))",
22 | },
23 | primary: {
24 | DEFAULT: "hsl(var(--primary))",
25 | foreground: "hsl(var(--primary-foreground))",
26 | },
27 | secondary: {
28 | DEFAULT: "hsl(var(--secondary))",
29 | foreground: "hsl(var(--secondary-foreground))",
30 | },
31 | muted: {
32 | DEFAULT: "hsl(var(--muted))",
33 | foreground: "hsl(var(--muted-foreground))",
34 | },
35 | accent: {
36 | DEFAULT: "hsl(var(--accent))",
37 | foreground: "hsl(var(--accent-foreground))",
38 | },
39 | destructive: {
40 | DEFAULT: "hsl(var(--destructive))",
41 | foreground: "hsl(var(--destructive-foreground))",
42 | },
43 | border: "hsl(var(--border))",
44 | input: "hsl(var(--input))",
45 | ring: "hsl(var(--ring))",
46 | chart: {
47 | 1: "hsl(var(--chart-1))",
48 | 2: "hsl(var(--chart-2))",
49 | 3: "hsl(var(--chart-3))",
50 | 4: "hsl(var(--chart-4))",
51 | 5: "hsl(var(--chart-5))",
52 | },
53 | },
54 | },
55 | },
56 | plugins: [require("tailwindcss-animate")],
57 | };
58 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5 | "target": "ES2020",
6 | "useDefineForClassFields": true,
7 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "moduleDetection": "force",
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 | "exactOptionalPropertyTypes": true,
20 |
21 | /* Linting */
22 | "strict": true,
23 | "noUnusedLocals": true,
24 | "noUnusedParameters": true,
25 | "noFallthroughCasesInSwitch": true,
26 | "baseUrl": "./",
27 | "paths": {
28 | "@/*": ["src/*"]
29 | }
30 | },
31 | "include": ["src"]
32 | }
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | }
8 | },
9 | "references": [
10 | {
11 | "path": "./tsconfig.app.json"
12 | },
13 | {
14 | "path": "./tsconfig.node.json"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "noEmit": true
11 | },
12 | "include": ["vite.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import react from "@vitejs/plugin-react-swc";
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | "@": path.resolve(__dirname, "./src"),
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------