Skip to content

Commit

Permalink
feat: navbar
Browse files Browse the repository at this point in the history
  • Loading branch information
warmachine028 committed Oct 25, 2024
1 parent baacae7 commit 990a8fe
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 77 deletions.
Binary file modified client/bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"lucide-react": "^0.453.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hotkeys-hook": "^4.5.1",
"react-intersection-observer": "^9.13.1",
"react-router-dom": "^6.27.0",
"tailwind-merge": "^2.5.4",
Expand Down
2 changes: 2 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { AppRouter } from '@/components'
import { ThemeProvider } from '@/providers'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import Navbar from './components/Navbar'

const ReactQueryDemoApp = () => {
return (
<BrowserRouter>
<Navbar />
<AppRouter />
</BrowserRouter>
)
Expand Down
58 changes: 15 additions & 43 deletions client/src/api/posts.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,67 @@
import axios from 'axios'
import { PostsResponse, Post } from '@/types'
import { handleApiError } from '@/lib/utils'

const api = axios.create({ baseURL: import.meta.env.VITE_API_URL })

export const getPosts = async (skip: number = 0, limit: number = 10): Promise<PostsResponse> => {
try {
const { data } = await api.get<PostsResponse>('/posts', {
params: { skip, limit }
})
const { data } = await api.get<PostsResponse>('/posts', { params: { skip, limit } })
return data
} catch (error) {
throw handleApiError(error)
}
}

export const createPost = async (post: Omit<Post, 'id' | 'views'>): Promise<Post> => {
export const searchPosts = async (query: string): Promise<PostsResponse> => {
try {
const { data } = await api.post<Post>('/posts', post)
const { data } = await api.get<PostsResponse>('/posts/search', { params: { q: query } })
return data
} catch (error) {
throw handleApiError(error)
}
}

export const updatePost = async (post: Partial<Post> & { id: number }): Promise<Post> => {
export const getPost = async (id: number): Promise<Post> => {
try {
const { data } = await api.put(`/posts/${post.id}`, post)
const { data } = await api.get<Post>(`/posts/${id}`)
return data
} catch (error) {
throw handleApiError(error)
}
}

export const deletePost = async (id: number): Promise<void> => {
export const createPost = async (post: Omit<Post, 'id' | 'views'>): Promise<Post> => {
try {
await api.delete(`/posts/${id}`)
const { data } = await api.post<Post>('/posts', post)
return data
} catch (error) {
throw handleApiError(error)
}
}

export const searchPosts = async (query: string): Promise<PostsResponse> => {
export const updatePost = async (post: Partial<Post>): Promise<Post> => {
try {
const { data } = await api.get<PostsResponse>(`/posts/search?q=${query}`)

const { data } = await api.put(`/posts/${post.id}`, post)
return data
} catch (error) {
throw handleApiError(error)
}
}

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

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

return {
...data,
posts: postsWithImages
}
await api.delete(`/posts/${id}`)
} catch (error) {
throw handleApiError(error)
}
}

export const updateReaction = async (postId: number, type: 'like' | 'dislike') => {
export const updateReaction = async (id: number, type: 'like' | 'dislike'): Promise<Post> => {
try {
const { data } = await api.put(`/posts/${postId}`, { reactions: { [type]: +1 } })

const { data } = await api.put(`/posts/${id}`, { reactions: { [type]: +1 } })
return data
} 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
}
16 changes: 9 additions & 7 deletions client/src/components/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ const AppRouter = () => {
<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 className="container mx-auto gap-5">
<div className="flex h-screen items-center justify-center space-x-2">
<Button asChild>
<Link to="/todos">Todos</Link>
</Button>
<Button asChild>
<Link to="/posts">Posts</Link>
</Button>
</div>
</div>
}
/>
Expand Down
20 changes: 13 additions & 7 deletions client/src/components/CreatePost.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef, useState } from 'react'
import { Button, Input, Textarea } from '@/components/ui'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { ListRestart, Loader2, Plus } from 'lucide-react'
import { ListRestart, Plus } from 'lucide-react'
import { useCreatePost } from '@/hooks'

const CreatePost = () => {
Expand All @@ -13,7 +13,7 @@ const CreatePost = () => {
}
const [post, setPost] = useState(initialData)
const fileInputRef = useRef<HTMLInputElement>(null)

const [error, setError] = useState('')
const addMutation = useCreatePost()

const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
Expand All @@ -33,10 +33,15 @@ const CreatePost = () => {
setPost({ ...post, [name]: value })
}
}
const validate = () => post.title.trim() && post.body.trim() && post.tags.trim() && post.image

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()

setError('')
if (!validate()) {
setError('Please fill in all fields and upload an image')
return setTimeout(() => setError(''), 5000)
}
addMutation.mutate({
title: post.title.trim(),
body: post.body.trim(),
Expand Down Expand Up @@ -80,7 +85,7 @@ const CreatePost = () => {
value={post.body}
onChange={handleChange}
placeholder="eg: Sunshine Radio is a radio station that plays music 24/7. It's a great place to relax and enjoy the music."
className="w-full min-h-24"
className="min-h-24 w-full"
/>
<Input
type="text"
Expand All @@ -100,12 +105,13 @@ const CreatePost = () => {
placeholder="eg: radio.png"
ref={fileInputRef}
/>
<Button type="reset" className="*:size-4" size="icon" title='reset'>
<Button type="reset" className="*:size-4" size="icon" title="reset">
<ListRestart />
</Button>
</div>
<Button type="submit" disabled={addMutation.isPending} className="w-full *:size-4 *:mr-2">
{addMutation.isPending ? <Loader2 className="animate-spin" /> : <Plus />}
<p className="text-destructive h-5 text-center">{error}</p>
<Button type="submit" className="w-full *:mr-2 *:size-4">
<Plus />
Add Post
</Button>
</form>
Expand Down
115 changes: 115 additions & 0 deletions client/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { useHotkeys } from 'react-hotkeys-hook'
import { Menu, Search, User, Home } from 'lucide-react'
import { Button, Input, ModeToggle } from '@/components/ui'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'

const AccountMenu = () => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" title="User menu">
<User />
<span className="sr-only">User menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-background/80 backdrop-blur-md" align="end">
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

const MobileMenu = () => {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon">
<Menu />
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-72">
<nav className="flex flex-col space-y-4">
<Link to="/" className="flex items-center py-2">
<Home className="mr-2" />
Home
</Link>
<Link to="/profile" className="flex items-center py-2">
<User className="mr-2" />
Profile
</Link>
<Link to="/settings" className="flex items-center py-2">
Settings
</Link>
</nav>
</SheetContent>
</Sheet>
)
}

const Navbar = () => {
const [isSearchOpen, setIsSearchOpen] = useState(false)

useHotkeys('ctrl+k', (event) => {
event.preventDefault()
setIsSearchOpen(true)
})

return (
<nav className="bg-background/80 sticky top-0 z-50 w-full border border-b backdrop-blur-md">
<div className="container mx-auto">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center">
<Link to="/" className="text-foreground flex items-center text-xl font-bold">
<img
src="https://query.gg/favicon.png"
alt="Brand"
className="mr-2 size-7 animate-spin [animation-duration:10s]"
/>
<span>React Query Demo</span>
</Link>
</div>

<div className="hidden items-center space-x-4 sm:flex">
<Input
type="search"
placeholder="Search (Ctrl + K)"
className="w-64"
onClick={() => setIsSearchOpen(true)}
/>
<ModeToggle />
<AccountMenu />
</div>

<div className="flex items-center space-x-2 sm:hidden">
<Button variant="ghost" size="icon" onClick={() => setIsSearchOpen(true)}>
<Search className="h-5 w-5" />
</Button>
<ModeToggle />
<MobileMenu />
</div>
</div>
</div>

{isSearchOpen && (
<div className="bg-background/80 fixed inset-0 z-50 flex items-start justify-center px-4 pt-16 backdrop-blur-sm">
<div className="bg-popover w-full max-w-2xl rounded-lg p-4 shadow-lg">
<Input
type="search"
placeholder="Search..."
className="w-full"
autoFocus
onBlur={() => setIsSearchOpen(false)}
/>
</div>
</div>
)}
</nav>
)
}

export default Navbar
2 changes: 1 addition & 1 deletion client/src/components/Post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const Post = ({ post }: { post: PostType }) => {
const deleteMutation = useDeletePost()
const reactionMutation = useUpdateReaction()

const handleUpdatePost = (post: PostType) => updateMutation.mutate({ ...post, tags: post.tags })
const handleUpdatePost = (post: PostType) => updateMutation.mutate(post)
const handleDeletePost = (id: number) => deleteMutation.mutate(id)

const handleReaction = (type: 'like' | 'dislike') => reactionMutation.mutate({ postId: post.id, type })
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/ui/mode-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTheme } from '@/hooks'

export const ModeToggle = ({ variant, className }: { variant?: 'default' | 'outline'; className?: string }) => {
const { setTheme, theme } = useTheme()
console.log(theme)

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand All @@ -21,7 +21,7 @@ export const ModeToggle = ({ variant, className }: { variant?: 'default' | 'outl
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent className="bg-background/80 backdrop-blur-md" align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
Expand Down
Loading

0 comments on commit 990a8fe

Please sign in to comment.