-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat 🎸 (be): add pinned to model and create migration * feat 🎸 (be): add route, service action and controller for handling post pin * refactor ✨ : move post pin action to own service * test 🧪 (unit): add service unit tests for pin and count * styles 💅 : add pin action on the UI * fix ✅ : temporarly disable CSRF * feat 🎸 : handle errors with toast * test 🧪 (browser): add tests for post/show pin actions * styles 💅 : improve load states * styles 💅 : re-styling improvements
- Loading branch information
1 parent
305a7ad
commit d18dff2
Showing
17 changed files
with
387 additions
and
38 deletions.
There are no files selected for viewing
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,31 @@ | ||
import { inject } from '@adonisjs/core' | ||
import policy from '#policies/posts_policy' | ||
import PostPinService from '#services/post_pin_service' | ||
import type { HttpContext } from '@adonisjs/core/http' | ||
import Post from '#models/post' | ||
|
||
@inject() | ||
export default class PostPinsController { | ||
constructor(private readonly service: PostPinService) {} | ||
|
||
async update(ctx: HttpContext) { | ||
const userId = ctx.auth.user?.id! | ||
const postId = ctx.params.id | ||
const post = await Post.findOrFail(postId) | ||
|
||
if (await ctx.bouncer.with(policy).denies('edit', post)) { | ||
return ctx.response.forbidden({ message: 'Not the author of this post.' }) | ||
} | ||
|
||
const pin = !post.pinned | ||
const count = await this.service.count(userId) | ||
|
||
if (count >= 2 && pin) { | ||
return ctx.response.conflict({ message: 'Exceeded max amount of pinned posts.' }) | ||
} | ||
|
||
const pinned = await this.service.pin(post, pin) | ||
|
||
return ctx.response.ok({ pinned }) | ||
} | ||
} |
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,26 @@ | ||
import Post from '#models/post' | ||
import db from '@adonisjs/lucid/services/db' | ||
import type { UUID } from 'node:crypto' | ||
|
||
export default class PostPinService { | ||
/** | ||
* Returns amount of posts pinned by a user. | ||
*/ | ||
async count(userId: UUID): Promise<number> { | ||
const query = (await db | ||
.from('posts') | ||
.where('user_id', userId) | ||
.andWhere('pinned', true) | ||
.count('*')) as [{ count: string }] | ||
return +query[0].count | ||
} | ||
|
||
/** | ||
* Handles the action of pinning a post | ||
*/ | ||
async pin(post: Post, pin: boolean): Promise<boolean> { | ||
post.pinned = pin | ||
await post.save() | ||
return pin | ||
} | ||
} |
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
17 changes: 17 additions & 0 deletions
17
database/migrations/1732792230151_add_pinned_flag_to_posts_table.ts
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,17 @@ | ||
import { BaseSchema } from '@adonisjs/lucid/schema' | ||
|
||
export default class extends BaseSchema { | ||
protected tableName = 'posts' | ||
|
||
async up() { | ||
this.schema.alterTable(this.tableName, (table) => { | ||
table.boolean('pinned').defaultTo(false) | ||
}) | ||
} | ||
|
||
async down() { | ||
this.schema.alterTable(this.tableName, (table) => { | ||
table.dropColumn('pinned') | ||
}) | ||
} | ||
} |
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,48 @@ | ||
import { useEffect, useRef } from 'react' | ||
import { Button } from '@/components/ui/button' | ||
import { ArrowUpFromDot } from 'lucide-react' | ||
|
||
export default function ScrollTop() { | ||
const topRef = useRef<HTMLButtonElement>(null) | ||
|
||
function scrollTop() { | ||
if (!window) return | ||
window.scrollTo({ | ||
top: 0, | ||
behavior: 'smooth', | ||
}) | ||
} | ||
|
||
useEffect(() => { | ||
window.addEventListener('scroll', (event) => { | ||
// @ts-ignore | ||
const scroll = event.srcElement.scrollingElement.scrollTop | ||
const classList = topRef.current?.classList | ||
|
||
const inClasses = ['fade-in', 'animate-in', 'spin-in-90'] | ||
const outClasses = ['fade-out', 'animate-out', 'spin-out-90'] | ||
|
||
if (scroll > 1000) { | ||
outClasses.map((className) => classList?.remove(className)) | ||
inClasses.map((className) => classList?.add(className)) | ||
} else { | ||
inClasses.map((className) => classList?.remove(className)) | ||
outClasses.map((className) => classList?.add(className)) | ||
} | ||
}) | ||
}, []) | ||
|
||
return window ? ( | ||
<Button | ||
ref={topRef} | ||
onClick={scrollTop} | ||
size="icon" | ||
className="fixed right-4 bottom-4 z-10 duration-1000" | ||
type="button" | ||
> | ||
<ArrowUpFromDot size={15} /> | ||
</Button> | ||
) : ( | ||
<></> | ||
) | ||
} |
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,13 +1,21 @@ | ||
import { UserResponse } from '#interfaces/user' | ||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' | ||
import { cn } from '@/lib/utils' | ||
import { useState } from 'react' | ||
|
||
export const UserAvatar = ({ user, className }: { user: UserResponse; className?: string }) => ( | ||
<Avatar className={cn('h-6 w-6', className)}> | ||
<AvatarImage | ||
src={user?.attachments ? user?.attachments?.avatar?.link : '#'} | ||
alt={`${user.fullname}'s avatar image`} | ||
/> | ||
<AvatarFallback>{user.fullname[0] || '-'}</AvatarFallback> | ||
</Avatar> | ||
) | ||
export const UserAvatar = ({ user, className }: { user: UserResponse; className?: string }) => { | ||
const [avatarLoadState, setAvatarLoadState] = useState<'loading' | 'loaded'>('loading') | ||
return ( | ||
<Avatar className={cn('h-6 w-6', className)}> | ||
<AvatarImage | ||
onLoad={() => setAvatarLoadState('loaded')} | ||
src={user?.attachments ? user?.attachments?.avatar?.link : '#'} | ||
alt={`${user.fullname}'s avatar image`} | ||
className={cn( | ||
avatarLoadState === 'loaded' ? 'block animate-in fade-in duration-1000' : 'hidden' | ||
)} | ||
/> | ||
<AvatarFallback>{user.fullname[0] || '-'}</AvatarFallback> | ||
</Avatar> | ||
) | ||
} |
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.