Skip to content

Commit

Permalink
feat: optimistic updates
Browse files Browse the repository at this point in the history
  • Loading branch information
warmachine028 committed Oct 24, 2024
1 parent ae86ff8 commit 53ffd55
Show file tree
Hide file tree
Showing 18 changed files with 1,149 additions and 8 deletions.
Binary file modified client/bun.lockb
Binary file not shown.
9 changes: 9 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,28 @@
"preview": "vite preview"
},
"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-slot": "^1.1.0",
"@tanstack/react-query": "^5.59.16",
"@tanstack/react-query-devtools": "^5.59.16",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^11.11.9",
"lucide-react": "^0.453.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intersection-observer": "^9.13.1",
"react-router-dom": "^6.27.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@tanstack/eslint-plugin-query": "^5.59.7",
"@types/node": "^22.7.9",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion client/src/App.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#root {
max-width: 1280px;
/* max-width: 1280px; */
margin: 0 auto;
padding: 2rem;
text-align: center;
Expand Down
18 changes: 15 additions & 3 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { BrowserRouter } from 'react-router-dom'
import { AppRouter } from '@/components'
import { ThemeProvider } from '@/providers'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const App = () => {
const ReactQueryDemoApp = () => {
return (
<BrowserRouter>
<AppRouter />
</BrowserRouter>
)
}

const App = () => {
const queryClient = new QueryClient()
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<AppRouter />
<ReactQueryDemoApp />
</ThemeProvider>
</BrowserRouter>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}

Expand Down
2 changes: 2 additions & 0 deletions client/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './todos'
export * from './posts'
134 changes: 134 additions & 0 deletions client/src/api/posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import axios from 'axios'

interface Post {
id: number
title: string
body: string
userId: number
imageUrl: string
tags: string[]
reactions: {
likes: number
dislikes: number
}
views: number
}

interface PostsResponse {
posts: Post[]
total: number
skip: number
limit: number
}

const API_URL = 'https://dummyjson.com'

export const getPosts = async (skip: number = 0, limit: number = 10): Promise<PostsResponse> => {
try {
const { data } = await axios.get<PostsResponse>(`${API_URL}/posts?limit=${limit}&skip=${skip}`)

// Add imageUrl and transform reactions to the required format for each post
const postsWithImages = data.posts.map((post) => ({
...post,
imageUrl: `https://picsum.photos/seed/${post.id}/800/600`
}))

return {
...data,
posts: postsWithImages
}
} catch (error) {
throw handleApiError(error)
}
}

// Other API functions remain similar but need to be updated with the new Post interface
export const createPost = async (post: Omit<Post, 'id' | 'imageUrl' | 'views'>): Promise<Post> => {
try {
const { data } = await axios.post(`${API_URL}/posts/add`, post)
return {
...data,
imageUrl: `https://picsum.photos/seed/${data.id}/800/600`,
views: 0,
reactions: { likes: 0, dislikes: 0 }
}
} catch (error) {
throw handleApiError(error)
}
}

export const updatePost = async (post: Partial<Post> & { id: number }): Promise<Post> => {
try {
const { data } = await axios.put(`${API_URL}/posts/${post.id}`, post)
return {
...data,
imageUrl: `https://picsum.photos/seed/${data.id}/800/600`,
reactions: post.reactions || { likes: 0, dislikes: 0 },
views: post.views || 0
}
} catch (error) {
throw handleApiError(error)
}
}

// Delete a post
export const deletePost = async (id: number): Promise<void> => {
try {
await axios.delete(`${API_URL}/posts/${id}`)
} catch (error) {
throw handleApiError(error)
}
}

// Search posts
export const searchPosts = async (query: string): Promise<PostsResponse> => {
try {
const { data } = await axios.get<PostsResponse>(`${API_URL}/posts/search?q=${query}`)

const postsWithImages = data.posts.map((post) => ({
...post,
imageUrl: `https://picsum.photos/seed/${post.id}/800/600`
}))

return {
...data,
posts: postsWithImages
}
} catch (error) {
throw handleApiError(error)
}
}

// Get posts by user
export const getPostsByUser = async (userId: number): Promise<PostsResponse> => {
try {
const { data } = await axios.get<PostsResponse>(`${API_URL}/posts/user/${userId}`)

const postsWithImages = data.posts.map((post) => ({
...post,
imageUrl: `https://picsum.photos/seed/${post.id}/800/600`
}))

return {
...data,
posts: postsWithImages
}
} catch (error) {
throw handleApiError(error)
}
}

// Error handling helper
const handleApiError = (error: unknown) => {
if (axios.isAxiosError(error)) {
const message = error.response?.data?.message || error.message
console.error('API Error:', {
status: error.response?.status,
message,
details: error.response?.data
})
throw new Error(`API Error: ${message}`)
}
console.error('Unexpected error:', error)
throw error
}
38 changes: 38 additions & 0 deletions client/src/api/todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// src/api/index.ts

import axios from 'axios'

// Define Todo type
interface Todo {
id: number
title: string
completed?: boolean
}

// API base URL - replace with your actual API endpoint
const API_URL = 'https://jsonplaceholder.typicode.com'

// Get all todos
export const getTodos = async (): Promise<Todo[]> => {
const { data } = await axios.get(`${API_URL}/todos`)
return data
}

// Add a new todo
export const postTodo = async (todo: Omit<Todo, 'completed'>): Promise<Todo> => {
const { data } = await axios.post(`${API_URL}/todos`, {
...todo,
completed: false
})
return data
}

// Optional: You might also want these additional methods
export const updateTodo = async (todo: Todo): Promise<Todo> => {
const { data } = await axios.put(`${API_URL}/todos/${todo.id}`, todo)
return data
}

export const deleteTodo = async (id: number): Promise<void> => {
await axios.delete(`${API_URL}/todos/${id}`)
}
21 changes: 18 additions & 3 deletions client/src/components/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { Route, Routes, useLocation } from 'react-router-dom'
import { Vite } from '@/pages'

import { Link, Route, Routes, useLocation } from 'react-router-dom'
import { Posts, Todos, Vite } from '@/pages'
import { Button } from '@/components/ui'
const AppRouter = () => {
const location = useLocation()
return (
<Routes location={location}>
<Route path="/vite" element={<Vite />} />
<Route
path="/"
element={
<div className="flex gap-5">
<Button asChild>
<Link to="/todos">Todos</Link>
</Button>
<Button asChild>
<Link to="/posts">Posts</Link>
</Button>
</div>
}
/>
<Route path="/todos" element={<Todos />} />
<Route path="/posts" element={<Posts />} />
</Routes>
)
}
Expand Down
29 changes: 29 additions & 0 deletions client/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'

import { cn } from '@/lib/utils'

const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground'
}
},
defaultVariants: {
variant: 'default'
}
}
)

export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}

const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(({ className, variant, ...props }, ref) => {
return <div className={cn(badgeVariants({ variant }), className)} ref={ref} {...props} />
})
Badge.displayName = 'Badge'
export { Badge, badgeVariants }
28 changes: 28 additions & 0 deletions client/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"

import { cn } from "@/lib/utils"

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
Loading

0 comments on commit 53ffd55

Please sign in to comment.