Skip to content

Commit

Permalink
[SOA-3] Create user page feed (#12)
Browse files Browse the repository at this point in the history
* feature: add user profile

* refactor: make navbar visible in all auth cases

* refactor: layout and route nits, and place back favicon as vite public asset

* tests: add user-feed acess minimal browser specs

* fix: solve routing issue

* style: more nits

* fix: include favicon in bundle
  • Loading branch information
mariadriana-deemaze authored Nov 7, 2024
1 parent be0943a commit 1a95208
Show file tree
Hide file tree
Showing 19 changed files with 306 additions and 165 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ tmp
*.env*
!.env.example

# Frontend assets compiled code
public/assets

# Build tools specific
npm-debug.log
yarn-error.log
Expand Down
18 changes: 18 additions & 0 deletions app/controllers/users_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import User from '#models/user';
import PostsService from '#services/posts_service';
import { inject } from '@adonisjs/core';
import type { HttpContext } from '@adonisjs/core/http'

@inject()
export default class UsersController {

constructor(public readonly service: PostsService) { }

async show(ctx: HttpContext) {
const profileId = ctx.params.id;
const page = ctx.request.qs().page || 1
const posts = await this.service.findMany(profileId, { page })
const profile = await User.find(profileId);
return ctx.inertia.render('users/show', { posts, profile })
}
}
2 changes: 1 addition & 1 deletion app/exceptions/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class HttpExceptionHandler extends ExceptionHandler {
*/
async handle(error: unknown & { name: string }, ctx: HttpContext) {
if (error.name === 'E_UNAUTHORIZED_ACCESS') {
return ctx.response.redirect('auth/sign-in')
return ctx.response.redirect('/auth/sign-in')
}
return super.handle(error, ctx)
}
Expand Down
2 changes: 1 addition & 1 deletion app/middleware/auth_middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class AuthMiddleware {
/**
* The URL to redirect to, when authentication fails
*/
redirectTo = 'auth/sign-in'
redirectTo = '/auth/sign-in'

async handle(
ctx: HttpContext,
Expand Down
12 changes: 12 additions & 0 deletions app/services/posts_service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Post from '#models/post'
import { createPostValidator, updatePostValidator } from '#validators/post'
import { ModelPaginatorContract } from '@adonisjs/lucid/types/model'
import type { UUID } from 'crypto'

export default class PostsService {
Expand Down Expand Up @@ -45,4 +46,15 @@ export default class PostsService {
const result: Post[] | null = await Post.query().where('id', id).preload('user');
return !!result ? result[0] : null;
}

/**
* Returns a paginated collection of posts, matching the search criteria.
*/
async findMany(userId: UUID, { page, limit = 10 }: { page: number, limit?: number }): Promise<ModelPaginatorContract<Post>> {
return Post.query()
.where('user_id', userId)
.orderBy('updated_at', 'desc')
.preload('user')
.paginate(page, limit)
}
}
11 changes: 3 additions & 8 deletions config/inertia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,14 @@ const inertiaConfig = defineConfig({
* Data that should be shared with all rendered pages
*/
sharedData: {
user: async (ctx): Promise<User | null> => {
if (!ctx?.auth?.user) {
return null
}
return ctx.auth.user
},
user: async (ctx): Promise<User | null> => ctx?.auth?.user || null,
errors: (ctx) => ctx.session?.flashMessages.get('errors'),
},

/**
* Options for the server-side rendering
*/
ssr: {
ssr: {
enabled: true,
entrypoint: 'inertia/app/ssr.tsx',
},
Expand All @@ -33,5 +28,5 @@ const inertiaConfig = defineConfig({
export default inertiaConfig

declare module '@adonisjs/inertia/types' {
export interface SharedProps extends InferSharedProps<typeof inertiaConfig> {}
export interface SharedProps extends InferSharedProps<typeof inertiaConfig> { }
}
4 changes: 3 additions & 1 deletion inertia/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Toaster } from '@/components/ui/toaster'
import { usePage } from '@inertiajs/react'
import UserNavBar from '@/components/users/nav'
import type { SharedProps } from '@adonisjs/inertia/types'
import favicon from '../../public/assets/images/favicon.svg'

export default function Layout({ children }: { children: ReactNode }) {
const {
Expand All @@ -11,7 +12,8 @@ export default function Layout({ children }: { children: ReactNode }) {

return (
<>
{user && <UserNavBar user={user} />}
<link rel="icon" type="image/svg+xml" href={favicon} />
<UserNavBar user={user} />
<div className="container flex justify-start pt-20">{children}</div>
<Toaster />
</>
Expand Down
82 changes: 82 additions & 0 deletions inertia/components/posts/feed-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect, useMemo, useState } from 'react'
import { router } from '@inertiajs/react'
import { useToast } from '@/components/ui/use-toast'
import PostCard from '@/components/posts/post-card'
import { useIntersectionObserver } from '@/hooks/use-intersection-observer'
import { Loader2 } from 'lucide-react'
import { ModelObject } from '@adonisjs/lucid/types/model'

export default function FeedList({
url,
posts,
currentUser,
}: {
url: string
posts: {
meta: any
data: ModelObject[]
}
currentUser: {
[x: string]: any
} | null
}) {
const [allPosts, setAllPosts] = useState(posts?.data)
const [meta, setMeta] = useState(posts.meta)

const { toast } = useToast()

const { isIntersecting, ref } = useIntersectionObserver({
threshold: 0.5,
})

function loadMore() {
router.get(
`${url}${meta.nextPageUrl}`,
{},
{
preserveState: true,
preserveScroll: true,
onSuccess: () => {
setAllPosts((prev) => [...(prev ?? []), ...posts.data])
setMeta(posts.meta)
},
onError: () => {
toast({ title: 'Error loading next page.' })
},
}
)
}

const hasMorePosts = useMemo(() => !!meta?.nextPageUrl, [meta])

useEffect(() => {
if (isIntersecting) loadMore()
}, [isIntersecting])

return (
<div className="feed-list w-full">
{!allPosts ? (
<Loader2 className="h-5 w-5 mr-2 animate-spin text-muted" />
) : (
allPosts?.map((post, index) => <PostCard user={currentUser} post={post} key={index} />)
)}
<div className="flex justify-center py-5 w-full min-w-full">
{posts?.data?.length > 0 ? (
<>
{hasMorePosts ? (
<p ref={ref} className="text-sm text-gray-600 self-center cursor-pointer">
fetch more around here
</p>
) : (
<p className="text-sm text-gray-600 self-center">Go touch grass outside.</p>
)}
</>
) : (
<div className="flex w-full hn-screen items-center justify-center">
<p className="text-sm text-gray-600 self-center">Nothing to see here. 🍃</p>
</div>
)}
</div>
</div>
)
}
12 changes: 8 additions & 4 deletions inertia/components/posts/post-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { ModelObject } from '@adonisjs/lucid/types/model'
import { UpdatePost } from '@/components/posts/update'
import { DeletePost } from '@/components/posts/delete'
import { formatDistanceToNow } from 'date-fns'

const userLink = (id: UUID) => `/users/${id}`
const postLink = (id: UUID) => `/posts/${id}`
Expand Down Expand Up @@ -38,10 +39,10 @@ export default function PostCard({

{showActions && (
<div className="flex flex-row gap-2">
<Button className='update-post-trigger' variant="outline" size="sm-icon">
<Button className="update-post-trigger" variant="outline" size="sm-icon">
<UpdatePost post={post} />
</Button>
<Button className='delete-post-trigger' variant="outline" size="sm-icon">
<Button className="delete-post-trigger" variant="outline" size="sm-icon">
<DeletePost post={post} />
</Button>
</div>
Expand Down Expand Up @@ -79,8 +80,11 @@ export default function PostCard({
{post.updatedAt && (
<span className="flex text-xs text-gray-500 gap-3 items-center">
<Clock size={12} />
{new Date(post.createdAt).toUTCString()}
{/* {new Date(post.updatedAt).toUTCString()} */}
{formatDistanceToNow(
//new Date(2014, 6, 2)
new Date(post.createdAt)
)}{' '}
ago
</span>
)}
</div>
Expand Down
80 changes: 43 additions & 37 deletions inertia/components/users/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { cn } from '@/lib/utils'
import User from '#models/user'
import AdonisLogo from '@/components/svg/logo'

export default function UserNavBar({ user }: { user: User }) {
export default function UserNavBar({ user }: { user: User | null }) {
const LINKS: Record<'title' | 'link', string>[] = [
{
title: 'Home',
Expand Down Expand Up @@ -48,44 +48,50 @@ export default function UserNavBar({ user }: { user: User }) {
))}
</nav>
<div className="ml-auto flex items-center space-x-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src="#" alt={`${user.name} avatar image`} />
<AvatarFallback>{user.name ? user.name[0] : '-'}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{user.name} {user.surname}
</p>
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{user ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src="#" alt={`${user.name} avatar image`} />
<AvatarFallback>{user.name ? user.name[0] : '-'}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{user.name} {user.surname}
</p>
<p className="text-xs truncate leading-none text-muted-foreground">{user.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Link href={`/users/${user.id}`}>Profile</Link>
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
<Link as="button" href={'/auth/sign-out'} method="delete">
Log out
</Link>
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link as="button" href={'/auth/sign-out'} method="delete">
Log out
</Link>
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Link href='/auth/sign-in'>
<Button size="sm">Sign in</Button>
</Link>
)}
</div>
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion inertia/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,16 @@ a {
}

.feed-list {
@apply my-5 flex flex-col gap-2 w-full pb-20;
@apply mb-5 flex flex-col gap-2 w-full pb-20;
}

.default-dialog {
@apply max-w-[calc(100vw_-_20px)] md:max-w-[425px] rounded-lg
}

.hn-screen {
@apply h-[calc(100vh_-_120px)]
}
}

@layer utilities {
Expand Down
Loading

0 comments on commit 1a95208

Please sign in to comment.