generated from warmachine028/github-super-starter-kit
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
baacae7
commit 990a8fe
Showing
14 changed files
with
319 additions
and
77 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.