Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5bc7f0a
feat(clubInfo): check if the user is president
at-wr Apr 15, 2024
6b7d175
feat(clubInfo): add component `ViewClubInfo`
at-wr Apr 15, 2024
09557f2
feat(clubInfo): add edit page for club details
at-wr Apr 15, 2024
9ad63f7
feat(clubInfo): add clubInfo section for /cas/clubs/[id]
at-wr Apr 15, 2024
8bdaa8e
fix(ESLint): fix style/indent
at-wr Apr 15, 2024
5ea0d8d
fix(LeaveRequest): fix excessive stack depth
at-wr Apr 15, 2024
4c8f3fe
fix(LeaveRequest): fix excessive stack depth
at-wr Apr 15, 2024
8f5f1f8
feat(clubInfo): add edit and ViewClubInfo components to club page
at-wr Apr 16, 2024
3fb2009
fix(clubInfo): permission error
at-wr Apr 16, 2024
dfb6d45
feat(clubInfo): judge if the supervisor have nickname
at-wr Apr 16, 2024
1e9569f
fix(clubInfo): fix privilege api method
at-wr Apr 16, 2024
a9f1cef
feat(clubInfo): show 404 when `isPresident` is false
at-wr Apr 16, 2024
4565e08
fix(clubInfo): let title
at-wr Apr 16, 2024
d1cf356
feat(clubInfo): only show `ViewClubInfo` if the club still exists
at-wr Apr 16, 2024
cc3342c
style(clubInfo): unify club information space style
at-wr Apr 16, 2024
87a4d64
style(clubInfo): unify club information space style
at-wr Apr 16, 2024
3fd4fde
refactor: use `user/all_clubs` for getting editable clubs
qwerzl Apr 19, 2024
0603640
refactor: use `user/all_clubs` for getting editable clubs
qwerzl Apr 19, 2024
16d7593
feat(clubInfo): add dialog for editing club info
at-wr May 9, 2024
cb967b0
feat(clubInfo): display QR Code in ViewClubInfo
at-wr May 9, 2024
4b45f36
feat: add some packages
at-wr May 9, 2024
30b3c14
fix: remove test condition
at-wr May 11, 2024
ae3bf1a
feat(club): show club name in title
at-wr May 20, 2024
cfc3b3f
chore: update pnpm-lock
at-wr May 20, 2024
699bfba
feat: teacher hover card
at-wr May 20, 2024
4be6f0a
style: add underline when hovered
at-wr May 20, 2024
45e408f
style: add hover placeholder for club members
at-wr May 20, 2024
cb53a39
feat: add found time for club info
at-wr May 20, 2024
eeedbdf
feat: add Open Graph meta data
at-wr May 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions components/custom/CAS/Info/ViewClubInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'

const props = defineProps({
club: {
type: Number,
required: true,
},
})

definePageMeta({
middleware: ['auth'],
})

const { data } = await useAsyncData('allInfo', () => {
return $fetch('/api/cas/info/get', {
headers: useRequestHeaders(),
method: 'GET',
body: {
club: props.club,
},
})
})

let noGroup = false

if (!data.value)
noGroup = true
</script>

<template>
<Card class="w-full">
<CardHeader>
<CardTitle class="flex items-center h-min gap-x-1">
社团群聊
</CardTitle>
<CardDescription class="flex items-center">
<Icon name="material-symbols:info-outline" />
<div class="ml-1">
Club Group
</div>
</CardDescription>
</CardHeader>
<CardContent v-if="!noGroup">
<div>微信群聊链接: {{ data }}</div>
</CardContent>
<CardContent v-if="noGroup">
<div class="text-sm italic text-muted-foreground text-center w-full my-2">
暂无内容 ╥﹏╥...
</div>
</CardContent>
</Card>
</template>
2 changes: 1 addition & 1 deletion components/custom/CAS/Leave/NewLeaveRequest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const formSchema = toTypedSchema(z.object({
}))

const { data } = await useAsyncData<AllClubs>('allClubs', () => {
return $fetch('/api/user/all_clubs', {
return $fetch<AllClubs>('/api/user/all_clubs', {
headers: useRequestHeaders(),
method: 'GET',
})
Expand Down
2 changes: 1 addition & 1 deletion components/custom/CAS/Leave/ViewMyLeaveRequests.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ definePageMeta({
})

const { data, refresh } = await useAsyncData<MyRequests>('allRequests', () => {
return $fetch('/api/cas/leave/my', {
return $fetch<MyRequests>('/api/cas/leave/my', {
headers: useRequestHeaders(),
method: 'GET',
})
Expand Down
181 changes: 181 additions & 0 deletions pages/cas/clubs/[id]/edit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { format } from 'date-fns'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import Calendar from '../../../../components/ui/calendar/Calendar.vue'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'
import { Button } from '~/components/ui/button'
import { cn } from '~/lib/utils'
import { Textarea } from '~/components/ui/textarea'
import { useToast } from '~/components/ui/toast/use-toast'
import { Toaster } from '~/components/ui/toast'
import type { Club, Clubs } from '~/types/clubs'

const emit = defineEmits(['refresh'])
const { data } = await useFetch<Clubs>('/api/club/all_details')
const clubs = data.value!
const route = useRoute()
const id = route.params.id // Fetch current Club ID via route params

// Filter clubs based on C_GroupsID and include information at the same level as groups
const filteredClubs = Object.values(clubs).flatMap(clubCategory =>
clubCategory.filter((club: Club) =>
club.groups.some(group => group.C_GroupsID === id),
).map((club: Club) => ({
...club, // Spread to include all same-level information
groups: club.groups.filter(group => group.C_GroupsID === id), // Filter groups to only include those that match the ID
})),
) as Club[]

const { toast } = useToast()

definePageMeta({
middleware: ['auth'],
})

const isLoading = ref(false)

// Check if the Club have Group Information
const { data: groupValue } = await useAsyncData('allInfo', () => $fetch('/api/cas/info/get', { headers: useRequestHeaders(), method: 'GET', body: { club: id } }))
const noGroup = !groupValue.value

const formSchema = toTypedSchema(z.object({
clubId: z.number(),
wechatGroupUrl: z.string().startsWith('https://weixin.qq.com/g/', { message: 'WeChat Info URL required' }),
wechatGroupExpiration: z.date(),
}))

const { handleSubmit, resetForm } = useForm({
validationSchema: formSchema,
})

const onSubmit = handleSubmit(async (values) => {
isLoading.value = true
values.clubId = Number(id)
const apiUrl = noGroup ? '/api/cas/info/new' : '/api/cas/info/update'
const { error } = await useFetch(apiUrl, {
headers: useRequestHeaders(),
method: 'post',
server: false,
body: values,
})
if (error.value) {
toast({
title: '错误',
description: '请稍后再试',
variant: 'destructive',
})
}
isLoading.value = false
emit('refresh')
resetForm()
})

// Get privilege information
const { data: privilegeData } = await useFetch<{ isPresident: boolean }>('/api/cas/info/privilege', {
method: 'GET',
body: {
clubId: Number(id),
},
headers: {
'Content-Type': 'application/json',
},
})

const isPresident = privilegeData.value?.isPresident || false

let title

if (isPresident)
title = 'Edit Club Info | Enspire'
else
title = '404 Not Found | Enspire'

useHead({
title,
})
</script>

<template>
<div v-if="isPresident">
<Card class="w-full h-min mt-2">
<CardHeader>
<CardTitle v-if="noGroup" class="flex items-center h-min gap-x-1">
添加 {{ filteredClubs[0].groups[0].C_NameC }} 群聊信息
</CardTitle>
<CardTitle v-if="!noGroup" class="flex items-center h-min gap-x-1">
编辑 {{ filteredClubs[0].groups[0].C_NameC }} 群聊信息
</CardTitle>
<CardDescription class="flex items-center">
<Icon name="material-symbols:info-outline" />
<div class="ml-1">
{{ filteredClubs[0].groups[0].C_NameE }} Group Info
</div>
</CardDescription>
</CardHeader>
<CardContent>
<form class="space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField, value }" name="wechatGroupExpiration">
<FormItem class="flex flex-col">
<FormLabel>失效日期 Expire Date</FormLabel>
<Popover>
<PopoverTrigger as-child>
<FormControl>
<Button
:class="cn(
'w-full ps-3 text-start font-normal',
!value && 'text-muted-foreground',
)" variant="outline"
:disabled="isLoading"
>
<span>{{ value ? format(value, "PPP") : "选择日期..." }}</span>
<Icon class="ms-auto opacity-50" name="material-symbols:calendar-today-outline" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent class="p-0">
<Calendar :min-date="new Date()" v-bind="componentField" />
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
</FormField>

<FormField v-slot="{ componentField }" name="wechatGroupUrl">
<FormItem>
<FormLabel>WeChat Group URL</FormLabel>
<FormControl>
<Textarea
class="resize-none"
placeholder="WeChat Group URL"
v-bind="componentField"
:disabled="isLoading"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>

<Button :disabled="isLoading" type="submit">
<Icon v-if="isLoading" class="mr-2" name="svg-spinners:180-ring-with-bg" />
提交
</Button>
</form>
</CardContent>
</Card>
<Toaster />
</div>
<div v-else>
<div class="flex flex-col justify-center h-1/2 text-center">
<h3 class="font-bold text-xl">
You shouldn't be here... ヾ(≧へ≦)〃
</h3>
<br>
<NuxtLink class="w-full mt-2" to="/">
<Button>回到主页</Button>
</NuxtLink>
</div>
</div>
</template>
83 changes: 56 additions & 27 deletions pages/cas/clubs/[id].vue → pages/cas/clubs/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { cleanHTML } from '@/lib/utils'
import type { Club, Clubs } from '~/types/clubs'
import ViewClubInfo from '~/components/custom/CAS/Info/ViewClubInfo.vue'

const { data } = await useFetch<Clubs>('/api/club/all_details')

Expand Down Expand Up @@ -37,6 +38,19 @@ if (filteredClubs[0] && filteredClubs[0].groups[0].C_DescriptionC) {
}
}

// Get privilege information
const { data: privilegeData } = await useFetch<{ isPresident: boolean }>('/api/cas/info/privilege', {
method: 'GET',
body: {
clubId: Number(id),
},
headers: {
'Content-Type': 'application/json',
},
})

const isPresident = privilegeData.value?.isPresident || false

// This page requires login
definePageMeta({
middleware: ['auth'],
Expand All @@ -61,6 +75,11 @@ useHead({
<Badge v-if="club.gmember.length === 0" variant="destructive">
已解散
</badge>
<NuxtLink v-if="isPresident" :to="`/cas/clubs/${id}/edit`">
<Button variant="outline">
编辑
</Button>
</NuxtLink>
</CardTitle>

<CardDescription class="flex items-center">
Expand Down Expand Up @@ -108,33 +127,43 @@ useHead({
</div>
</CardContent>
</Card>
<Card class="xl:w-1/4 w-full xl:ml-2 h-min">
<CardHeader>
<CardTitle class="flex items-center h-min gap-x-1">
社团属性
</CardTitle>
<CardDescription class="flex items-center">
<Icon name="material-symbols:info-outline" />
<div class="ml-1">
Club Information
</div>
</CardDescription>
</CardHeader>
<CardContent>
<div>
<span class="font-bold">社团类型</span>: {{ group.C_Category }}
</div>
<div>
<span class="font-bold">社团人数</span>: {{ groupMemberCounts }} 人
</div>
<div v-if="club.supervisor" class="flex">
<span class="font-bold">指导老师:</span>
<span v-for="supervisor in club.supervisor" :key="supervisor.TeacherID" class="ml-2">
{{ supervisor.T_Name }} ({{ supervisor.T_Nickname }})
</span>
</div>
</CardContent>
</Card>
<div class="xl:w-1/4 w-full xl:ml-2 h-min">
<div class="xl:w-full">
<Card class="w-full">
<CardHeader>
<CardTitle class="flex items-center h-min gap-x-1">
社团属性
</CardTitle>
<CardDescription class="flex items-center">
<Icon name="material-symbols:info-outline" />
<div class="ml-1">
Club Information
</div>
</CardDescription>
</CardHeader>
<CardContent>
<div>
<span class="font-bold mr-1">社团类型:</span>{{ group.C_Category }}
</div>
<div>
<span class="font-bold mr-1">社团人数:</span>{{ groupMemberCounts }} 人
</div>
<div v-if="club.supervisor" class="flex">
<span class="font-bold">指导老师:</span>
<span v-for="supervisor in club.supervisor" :key="supervisor.TeacherID" class="ml-1">
{{ supervisor.T_Name }}
<span v-if="supervisor.T_Nickname" class="text-muted-foreground">
({{ supervisor.T_Nickname }})
</span>
</span>
</div>
</CardContent>
</Card>
</div>
<div v-if="club.gmember.length > 0" class="xl:w-full mt-2">
<ViewClubInfo :club="id" />
</div>
</div>
</div>
<!-- <div style="display:none"> -->
<!-- <Card v-if="club.grecord.length > 0" class="w-full"> -->
Expand Down
34 changes: 34 additions & 0 deletions server/api/cas/info/privilege.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { PrismaClient } from '@prisma/client'
import * as z from 'zod'

const prisma = new PrismaClient()

const requestSchema = z.object({
clubId: z.number(),
})

export default eventHandler(async (event) => {
const { auth } = event.context

if (!auth.userId) {
setResponseStatus(event, 403)
return
}

const tsimsStudentId = (await prisma.user.findUnique({
where: {
id: auth.userId,
},
}))!.tsimsStudentId

const requestBody = await readValidatedBody(event, body => requestSchema.parse(body))

const club = (await prisma.club.findFirst({
where: {
id: requestBody.clubId,
presidentByTsimsStudentId: tsimsStudentId,
},
}))

return !!club
})