Skip to content

Commit

Permalink
feat: add vote toggle
Browse files Browse the repository at this point in the history
  • Loading branch information
KeziahMoselle committed Apr 9, 2024
1 parent 0cce9bd commit a2fc1d8
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 31 deletions.
1 change: 1 addition & 0 deletions backend/src/collections/EldenRing/Builds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const ERBuilds: CollectionConfig = {
relationTo: 'users',
hasMany: true,
maxDepth: 0,
unique: true,
admin: {
position: 'sidebar'
}
Expand Down
59 changes: 59 additions & 0 deletions backend/src/endpoints/toggle-vote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { PayloadHandler } from 'payload/config'
import { z, ZodError } from 'zod'

const bodySchema = z.object({
buildId: z.number(),
})

export const toggleVote: PayloadHandler = async (req, res): Promise<void> => {
const { user, payload } = req

if (!user) {
res.status(401).json({ message: 'Unauthorized' })
return
}

try {
const { buildId } = bodySchema.parse(req.body)

const build = await payload.findByID({
collection: 'er-builds',
id: buildId,
depth: 0,
})

const indexOfVote = build.votes.indexOf(user.id)

if (indexOfVote > -1) {
build.votes.splice(indexOfVote, 1)
} else {
build.votes.push(user.id)
}

const result = await payload.update({
collection: 'er-builds',
id:buildId,
depth: 0,
data: {
votes: build.votes,
votes_count: build.votes.length
}
})

res.json(result)
} catch (error) {
if (error instanceof ZodError) {
payload.logger.error(`[${req.body.email}] ${error.message} ${error.issues.map((e) => e.message).join(', ')}`)

res.status(400).json({
...error
})
return
}

if (error instanceof Error) {
payload.logger.error(`[${req.body.email}] ${error.message}`)
res.status(400).json({ ...error })
}
}
}
6 changes: 6 additions & 0 deletions backend/src/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { slateEditor } from '@payloadcms/richtext-slate'

import { seed } from './endpoints/seed'
import { register } from './endpoints/register'
import { toggleVote } from './endpoints/toggle-vote'

import Users from './collections/Users'
import Archetype from './collections/Archetype'
Expand Down Expand Up @@ -83,6 +84,11 @@ export default buildConfig({
method: 'post',
handler: register,
},
{
path: '/er-builds/toggle-vote',
method: 'post',
handler: toggleVote,
},
{
path: '/seed',
method: 'get',
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { PayloadCollection } from '@/types'
import type { ErBuild } from '~/payload-types'
import { toast } from 'vue-sonner'
import qs from 'qs'

export async function apiFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
return fetchJSON(`${import.meta.env.PUBLIC_PAYLOAD_URL}${endpoint}`, options)
Expand Down Expand Up @@ -111,4 +114,30 @@ export async function logout() {
return 'There was an error'
}
})
}

export async function getMostVotedBuilds({ limit }: { limit: number }) {
const stringifiedQuery = qs.stringify(
{
sort: 'votes',
limit,
},
{ addQueryPrefix: true },
)
const url = `/api/er-builds${stringifiedQuery}`

const builds = await apiFetch<PayloadCollection<ErBuild>>(url)

return builds
}

export async function toggleVoteBuild({ buildId }) {
const updatedBuild = await apiFetch<ErBuild>(`/api/er-builds/toggle-vote`, {
method: 'POST',
body: JSON.stringify({
buildId
})
})

return updatedBuild
}
35 changes: 26 additions & 9 deletions frontend/src/components/molecules/EldenRing/BuildCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
import { ThumbsUpIcon } from 'lucide-vue-next'
import EquipmentImage from '@/components/molecules/EldenRing/EquipmentImage.vue';
import { Vue3Marquee } from 'vue3-marquee'
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { toggleVoteBuild } from '@/api';
import { toast } from 'vue-sonner';
const props = defineProps<{
build?: ErBuild,
hasVoted?: boolean
}>()
const hasVoted = ref(props.hasVoted)
const votesCount = ref(props.build.votes_count)
const mainWeapons = computed(() => {
// Return the first mainhand and offhand if each have at least 1 weapon
if (props.build.mainhand_weapons.length > 0 && props.build.offhand_weapons.length > 0) {
Expand All @@ -22,16 +27,26 @@
// No offhand but at least 1 mainhand
if (props.build.mainhand_weapons.length > 0 && props.build.offhand_weapons.length === 0) {
return props.build.mainhand_weapons.slice(0, 1).filter(Boolean)
return props.build.mainhand_weapons.slice(0, 2).filter(Boolean)
}
if (props.build.offhand_weapons.length > 0 && props.build.mainhand_weapons.length === 0) {
return props.build.offhand_weapons.slice(0, 1).filter(Boolean)
return props.build.offhand_weapons.slice(0, 2).filter(Boolean)
}
})
async function vote() {}
async function unvote() {}
async function toggleVote() {
const update = toggleVoteBuild({ buildId: props.build.id })
toast.promise(update, {
success: (updatedBuild) => {
votesCount.value = updatedBuild.votes_count
hasVoted.value = !hasVoted.value
return `Successfully ${hasVoted.value ? 'voted' : 'removed vote'}.`
},
error: (e) => 'There was an error.'
})
}
</script>

<template>
Expand All @@ -49,7 +64,7 @@
absolute top-1/2 transform -translate-y-1/2 left-8
flex items-center gap-x-4
">
<img class="h-6 w-6" src="https://cdn.soulsborne.build/test%2Fbleed.png" alt="bleed" />
<img class="size-8" src="https://cdn.soulsborne.build/test%2Fbleed.png" alt="bleed" />
<p class="-ml-4 w-52 lg:w-48 2xl:w-64">
<Vue3Marquee
gradient
Expand All @@ -67,13 +82,15 @@
</div>

<button
class="flex items-center self-center gap-x-2 bg-accent-foreground leading-4 px-2 py-1 rounded transition lg:text-sm lg:px-3 hover:bg-accent"
type="button"
class="flex items-center self-center gap-x-2 leading-4 px-2 py-1 rounded transition lg:text-sm lg:px-3 hover:bg-accent"
:class="{
'bg-accent-foreground': !hasVoted,
'bg-accent': hasVoted
}"
:title="hasVoted ? 'Unvote' : 'Vote'"
@click="hasVoted ? unvote : vote">
<span class="type-h5">{{ build.votes_count }}</span>
@click="toggleVote">
<span class="type-h5">{{ votesCount }}</span>
<ThumbsUpIcon class="w-3" />
</button>
</header>
Expand Down
28 changes: 21 additions & 7 deletions frontend/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import { apiFetch } from "@/api";
import type { PayloadUserResponse } from "@/types";
import { defineMiddleware, sequence } from "astro:middleware"

const auth = defineMiddleware(async ({ cookies, locals }, next) => {
const auth = defineMiddleware(async ({ cookies, locals, redirect }, next) => {
if (!cookies.has('payload-token')) {
locals.user = null
return next()
}

const data: PayloadUserResponse = await fetch(`${import.meta.env.PUBLIC_PAYLOAD_URL}/api/users/me`, {
headers: {
'Cookie': `payload-token=${cookies.get('payload-token').value}`
},
}).then((res) => res.json())
let data: PayloadUserResponse
try {
data = await apiFetch('/api/users/me', {
headers: {
'Cookie': `payload-token=${cookies.get('payload-token').value}`
},
})
} catch (error) {
console.error(error)
return next()
}

// If data.user is null that means the token expired
// todo: Refresh token here
if (!data.user) {
cookies.delete('payload-token')
return redirect('/?logged-out')
}

locals.user = data?.user
locals.user = data.user
return next()
})

Expand Down
18 changes: 3 additions & 15 deletions frontend/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,10 @@
import BuildCard from "@/components/molecules/EldenRing/BuildCard.vue"
import Layout from "../layouts/Layout.astro"
import TranslateImages from '@/components/atoms/TranslateImages.vue'
import { apiFetch } from "@/api"
import type { PayloadCollection } from "@/types"
import type { ErBuild } from "~/payload-types"
import qs from 'qs'
import { getMostVotedBuilds } from "@/api"
const stringifiedQuery = qs.stringify(
{
sort: 'votes',
limit: 3,
},
{ addQueryPrefix: true },
)
const url = `/api/er-builds${stringifiedQuery}`
const builds = await apiFetch<PayloadCollection<ErBuild>>(url)
const builds = await getMostVotedBuilds({ limit: 3 })
---

<Layout title="Home">
<div class="relative">
<img class="-z-10 absolute top-0 w-full max-h-screen object-cover object-top" src="/hero/gideon.webp" alt="" />
Expand Down Expand Up @@ -92,7 +80,7 @@ const builds = await apiFetch<PayloadCollection<ErBuild>>(url)
{builds.docs.map((build) => (
<BuildCard
build={build}
hasVoted={build.votes.includes(Astro.locals.user.id)}
hasVoted={build.votes?.includes(Astro.locals.user?.id)}
client:visible />
))}
</div>
Expand Down

0 comments on commit a2fc1d8

Please sign in to comment.