diff --git a/.prettierrc b/.prettierrc index eff7b7c..ab10840 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,5 +7,8 @@ "trailingComma": "none", "bracketSpacing": true, "arrowParens": "always", - "endOfLine": "lf" + "endOfLine": "lf", + "plugins": [ + "prettier-plugin-tailwindcss" + ] } diff --git a/client/bun.lockb b/client/bun.lockb index 6e70273..6ed77dd 100644 Binary files a/client/bun.lockb and b/client/bun.lockb differ diff --git a/client/package.json b/client/package.json index 7e7dbc3..1989277 100644 --- a/client/package.json +++ b/client/package.json @@ -7,13 +7,15 @@ "dev": "bun x vite", "build": "bun x vite build", "lint": "eslint .", - "preview": "bun x vite preview" + "preview": "bun x vite preview", + "start": "bun x vite preview --host" }, "dependencies": { "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.59.16", "@tanstack/react-query-devtools": "^5.59.16", @@ -44,9 +46,12 @@ "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", "postcss": "^8.4.47", + "prettier-plugin-tailwindcss": "^0.6.8", "tailwindcss": "^3.4.14", - "typescript": "~5.6.3", "typescript-eslint": "^8.11.0", "vite": "6.0.0-beta.4" + }, + "peerDependencies": { + "typescript": "~5.6.3" } } \ No newline at end of file diff --git a/client/src/api/posts.ts b/client/src/api/posts.ts index 70cf3fc..d73cdfe 100644 --- a/client/src/api/posts.ts +++ b/client/src/api/posts.ts @@ -1,6 +1,5 @@ import axios from 'axios' import { PostsResponse, Post } from '@/types' -import { sleep } from '@/lib/utils' const api = axios.create({ baseURL: import.meta.env.VITE_API_URL }) @@ -41,17 +40,11 @@ export const deletePost = async (id: number): Promise => { } } -// Search posts export const searchPosts = async (query: string): Promise => { try { const { data } = await api.get(`/posts/search?q=${query}`) - const postsWithImages = data.posts.map((post) => ({ - ...post, - imageUrl: `https://picsum.photos/seed/${post.id}/800/600` - })) - - return { ...data, posts: postsWithImages } + return data } catch (error) { throw handleApiError(error) } @@ -78,7 +71,6 @@ export const getPostsByUser = async (userId: number): Promise => export const updateReaction = async (postId: number, type: 'like' | 'dislike') => { try { - await sleep(3000) const { data } = await api.put(`/posts/${postId}`, { reactions: { [type]: +1 } }) return data diff --git a/client/src/components/ui/index.ts b/client/src/components/ui/index.ts index 08ed825..79bd353 100644 --- a/client/src/components/ui/index.ts +++ b/client/src/components/ui/index.ts @@ -7,3 +7,4 @@ export { ScrollArea } from './scroll-area' export { Badge } from './badge' export { Textarea } from './textarea' export { Search } from './search' +export { Separator } from './separator' diff --git a/client/src/components/ui/mode-toggle.tsx b/client/src/components/ui/mode-toggle.tsx index 191832b..878a5f3 100644 --- a/client/src/components/ui/mode-toggle.tsx +++ b/client/src/components/ui/mode-toggle.tsx @@ -1,17 +1,23 @@ -import { Moon, Sun } from 'lucide-react' +import { Computer, Moon, Sun } from 'lucide-react' import { Button } from './button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './dropdown-menu' import { useTheme } from '@/hooks' export const ModeToggle = ({ variant, className }: { variant?: 'default' | 'outline'; className?: string }) => { - const { setTheme } = useTheme() - + const { setTheme, theme } = useTheme() + console.log(theme) return ( diff --git a/client/src/components/ui/search.tsx b/client/src/components/ui/search.tsx index 945cc09..8f9fc1d 100644 --- a/client/src/components/ui/search.tsx +++ b/client/src/components/ui/search.tsx @@ -18,5 +18,6 @@ const Search = ({ className, iconClassName, ...props }: { iconClassName?: string ) } +Search.displayName = 'Search' export { Search } diff --git a/client/src/components/ui/separator.tsx b/client/src/components/ui/separator.tsx new file mode 100644 index 0000000..6d7f122 --- /dev/null +++ b/client/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/client/src/index.css b/client/src/index.css index 35de7f0..b1ff4bc 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -29,6 +29,8 @@ a:hover { body { margin: 0; display: flex; + align-items: center; + justify-content: center; place-items: center; min-width: 320px; min-height: 100vh; diff --git a/client/src/pages/Posts.tsx b/client/src/pages/Posts.tsx index ddc4c4f..b3aa799 100644 --- a/client/src/pages/Posts.tsx +++ b/client/src/pages/Posts.tsx @@ -29,7 +29,7 @@ const PostsCard = () => {
Posts -
+
{status === 'pending' ? ( -
- +
+
) : status === 'error' ? ( -

Error fetching posts

+

Error fetching posts

) : ( {results.map((post) => ( @@ -83,8 +83,8 @@ const PostsCard = () => { )} {isFetchingNextPage && ( -
- +
+
)}
@@ -97,7 +97,7 @@ const PostsCard = () => { const Posts = () => { return (
-
+
diff --git a/client/src/pages/Todos.tsx b/client/src/pages/Todos.tsx index d226032..dc54daa 100644 --- a/client/src/pages/Todos.tsx +++ b/client/src/pages/Todos.tsx @@ -91,11 +91,11 @@ export default function Todos() { {status === 'pending' ? ( -
- +
+
) : status === 'error' ? ( -

Error fetching todos

+

Error fetching todos

) : (
    {data.pages.map((page, i) => ( @@ -103,9 +103,9 @@ export default function Todos() { {page.todos.map((todo) => (
  • -
    +
    {todo.title} @@ -127,7 +127,7 @@ export default function Todos() { onClick={() => handleDeleteTodo(todo.id)} className="text-destructive hover:text-destructive hover:bg-destructive/10" > - +
  • ))} @@ -136,8 +136,8 @@ export default function Todos() {
)} {isFetchingNextPage && ( -
- +
+
)}
@@ -155,15 +155,11 @@ export default function Todos() { diff --git a/client/src/pages/Vite.tsx b/client/src/pages/Vite.tsx index 5eb3b7b..4c32ead 100644 --- a/client/src/pages/Vite.tsx +++ b/client/src/pages/Vite.tsx @@ -1,42 +1,85 @@ import { useState } from 'react' -import { reactLogo } from '@/assets' +import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card' +import { Button, Separator, ModeToggle } from '@/components/ui' import { Link } from 'react-router-dom' -import { Button, Card, ModeToggle } from '@/components/ui' -import '@/App.css' +import { House } from 'lucide-react' +import { create } from 'zustand' -const Vite = () => { - const [count, setCount] = useState(0) +const logos = [ + { name: 'Vite', url: 'https://vite.dev', src: '/vite.svg' }, + { name: 'React Query', url: 'https://tanstack.com/query', src: 'https://query.gg/favicon.png' }, + { name: 'Zustand', url: 'https://zustand-demo.pmnd.rs', src: 'https://zustand-demo.pmnd.rs/favicon.ico' }, + { name: 'shadcn/ui', url: 'https://ui.shadcn.com', src: 'https://ui.shadcn.com/favicon.ico' }, + { name: 'React', url: 'https://react.dev', src: 'https://react.dev/favicon.ico' }, + { name: 'Axios', url: 'https://axios-http.com', src: 'https://axios-http.com/assets/favicon.ico' }, + { name: 'Tailwind CSS', url: 'https://tailwindcss.com', src: 'https://tailwindcss.com/favicons/favicon.ico' }, + { name: 'Lucide Icons', url: 'https://lucide.dev', src: 'https://lucide.dev/favicon.ico' }, + { name: 'TypeScript', url: 'https://www.typescriptlang.org', src: 'https://www.typescriptlang.org/favicon.ico' } +] - return ( - -
- - Vite logo - - - React logo - - - React Query logo - -
-

Vite + React

-
{count}
- - - +type Store = { + count: number + inc: () => void + dec: () => void +} -

- Edit src/App.tsx and save to test HMR -

+const useStore = create()((set) => ({ + count: 1, + inc: () => set((state) => ({ count: state.count + 1 })), + dec: () => set((state) => ({ count: state.count - 1 })) +})) -

Click on the Vite and React logos to learn more

-
+const TechStack = () => { + const { count, inc, dec } = useStore() + + return ( +
+ + + Tech Stack + + +
+ {logos.map((logo) => ( + + {`${logo.name} + {logo.name} + + ))} +
+ +
+
{count}
+
+ + + + +
+
+
+ +

+ Edit src/TechStack.tsx and save to test HMR +

+
+
+
) } -export default Vite +export default TechStack diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index cec05c4..8c9b8eb 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -1,29 +1,23 @@ -interface Post { - id: number - title: string - reactions: { likes: number; dislikes: number } - tags: string[] - userId: number - imageUrl: string -} +import type { Post, Request } from '@/types' -type GetPostsParams = { - query: { skip?: string; limit?: string } -} +const baseUrl = 'https://dummyjson.com' +const delay = 5000 -type UpdatePostParams = { - params: { id: number } - body: Post -} +export const getPosts = async ({ query: { skip, limit } }: Request): Promise => { + const response = await fetch(`${baseUrl}/posts?skip=${skip || 0}&limit=${limit || 10}`) + const data = await response.json() -type CreatePostParams = { - body: Omit + return { + ...data, + posts: data.posts.map((post: Post) => ({ + ...post, + imageUrl: `https://picsum.photos/seed/${post.id}/800/600` + })) + } } -const baseUrl = 'https://dummyjson.com' - -export const getPosts = async ({ query: { skip, limit } }: GetPostsParams): Promise => { - const response = await fetch(`${baseUrl}/posts?skip=${skip || 0}&limit=${limit || 10}`) +export const searchPosts = async ({ query: { q } }: Request): Promise => { + const response = await fetch(`${baseUrl}/posts/search?q=${q}&delay=${delay}`) const data = await response.json() return { @@ -34,14 +28,21 @@ export const getPosts = async ({ query: { skip, limit } }: GetPostsParams): Prom })) } } -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) -export const createPost = async ({ body: post }: CreatePostParams): Promise => { - await sleep(5000) - const response = await fetch(`${baseUrl}/posts/add`, { +export const getPost = async ({ params: { id } }: Request): Promise => { + const response = await fetch(`${baseUrl}/posts/${id}`) + const data = await response.json() + return { + ...data, + imageUrl: `https://picsum.photos/seed/${data.id}/800/600` + } +} + +export const createPost = async ({ body }: Request): Promise => { + const response = await fetch(`${baseUrl}/posts/add?delay=${delay}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(post) + body: JSON.stringify(body) }) const data = await response.json() return { @@ -50,12 +51,11 @@ export const createPost = async ({ body: post }: CreatePostParams): Promise => { - await sleep(5000) - const response = await fetch(`${baseUrl}/posts/${id}`, { +export const updatePost = async ({ body, params: { id } }: Request): Promise => { + const response = await fetch(`${baseUrl}/posts/${id}?delay=${delay}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(post) + body: JSON.stringify(body) }) const data = await response.json() return { @@ -63,3 +63,10 @@ export const updatePost = async ({ body: post, params: { id } }: UpdatePostParam imageUrl: `https://picsum.photos/seed/${data.id}/800/600` } } + +export const deletePost = async ({ params: { id } }: Request): Promise => { + const response = await fetch(`${baseUrl}/posts/${id}?delay=${delay}`, { method: 'DELETE' }) + return response.json() +} + + diff --git a/server/src/lib/index.ts b/server/src/lib/index.ts new file mode 100644 index 0000000..ae79457 --- /dev/null +++ b/server/src/lib/index.ts @@ -0,0 +1 @@ +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index 96c7e0f..b8219a0 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -1,20 +1,29 @@ import { Elysia, t } from 'elysia' -import { createPost, getPosts, updatePost } from '@/controllers' +import { createPost, deletePost, getPost, getPosts, searchPosts, updatePost } from '@/controllers' export const postRoutes = new Elysia({ prefix: '/posts' }) - // .get('/', getPosts, { query: t.Object({ skip: t.Optional(t.String()), limit: t.Optional(t.String()) }) }) + .get('/search', searchPosts, { + query: t.Object({ + q: t.String() + }) + }) + .get('/:id', getPost, { + params: t.Object({ + id: t.Number() + }) + }) .post('/', createPost, { body: t.Object({ title: t.String(), body: t.String(), + tags: t.Array(t.String()), userId: t.Number(), - tags: t.Array(t.String()) }) }) .put('/:id', updatePost, { @@ -23,5 +32,13 @@ export const postRoutes = new Elysia({ prefix: '/posts' }) body: t.String(), tags: t.Array(t.String()), userId: t.Number() + }), + params: t.Object({ + id: t.Number() + }) + }) + .delete('/:id', deletePost, { + params: t.Object({ + id: t.Number() }) }) diff --git a/server/src/types.d.ts b/server/src/types.d.ts new file mode 100644 index 0000000..0478107 --- /dev/null +++ b/server/src/types.d.ts @@ -0,0 +1,15 @@ +export interface Post { + id: number + title: string + reactions: { likes: number; dislikes: number } + tags: string[] + userId: number + imageUrl: string +} + + +export type Request = { + params: { id: number } + query: { skip?: string; limit?: string; q?: string } + body: Post +} \ No newline at end of file