Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(clubInfo): add clubInfo related pages #566

Open
wants to merge 29 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
226 changes: 226 additions & 0 deletions components/custom/CAS/Info/EditClubInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { useForm } from 'vee-validate'
import { format } from 'date-fns'
import { ref } from 'vue'
import { scan } from 'qr-scanner-wechat'
import Calendar from '../../../ui/calendar/Calendar.vue'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { useToast } from '~/components/ui/toast/use-toast'
import { Button } from '~/components/ui/button'
import { cn } from '~/lib/utils'

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

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

const { toast } = useToast()
const isLoading = ref(false)
const id = props.club
// const club = await getEditableClub(id)
const groupUrlValid = ref(false)

// if (!club) {
// throw createError({
// statusCode: 404,
// statusMessage: 'Page Not Found',
// })
// }

// 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
resetForm()
})

const form = ref({
wechatGroupUrl: '', // Initialize the form data
})

async function onFileChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file)
return

const reader = new FileReader()
reader.onload = async () => {
const dataUrl = reader.result as string
const img = new Image()
img.src = dataUrl

img.onload = async () => {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(img, 0, 0)
try {
const result = await scan(canvas)
if (result && result.text) {
form.value.wechatGroupUrl = result.text
groupUrlValid.value = true
toast({
title: 'Success',
description: 'URL detected from Image ;)',
})
}
else {
toast({
title: 'Warning',
description: 'No QR code found or no URL detected',
variant: 'destructive',
})
}
}
catch (error) {
toast({
title: 'Error',
description: 'Error scanning QR code. Please try again.',
variant: 'destructive',
})
}
}
else {
toast({
title: 'Error',
description: 'Error loading image. Please try again.',
variant: 'destructive',
})
}
}

img.onerror = () => {
toast({
title: 'Error',
description: 'Error loading image. Please try again.',
variant: 'destructive',
})
}
}
reader.readAsDataURL(file)
}
</script>

<template>
<Dialog>
<DialogTrigger as-child>
<Button variant="outline">
编辑信息
</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>修改社团信息</DialogTitle>
<DialogDescription>
您可在此处修改社团相关信息
</DialogDescription>
</DialogHeader>
<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 name="wechatGroupUrl">
<FormItem>
<FormLabel>
微信群聊
<Icon v-if="groupUrlValid" class="mr-2 h-4 w-4" name="material-symbols:done" />
</FormLabel>
<FormControl>
<input type="file" accept="image/*" :disabled="isLoading" @change="onFileChange">
</FormControl>
<FormMessage />
</FormItem>
</FormField>

<DialogFooter>
<Button :disabled="isLoading || !groupUrlValid" type="submit">
<Icon v-if="isLoading" class="mr-2" name="svg-spinners:180-ring-with-bg" />
提交
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>

<style scoped>

</style>
66 changes: 66 additions & 0 deletions components/custom/CAS/Info/ViewClubInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script setup lang="ts">
import {
renderSVG,
} from 'uqr'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'

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

let svg: string

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

let clubInfo: any

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

// eslint-disable-next-line prefer-const
clubInfo = data.value

let noGroup = false

if (!data.value)
noGroup = true

if (data.value)
svg = renderSVG(clubInfo.wechatGroupUrl)
</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 v-html="svg" />
</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
48 changes: 25 additions & 23 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,56 +14,58 @@
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"dependencies": {
"@clerk/clerk-sdk-node": "^4.13.14",
"@clerk/clerk-sdk-node": "^4.13.15",
"@nuxt/content": "^2.12.1",
"@radix-icons/vue": "^1.0.0",
"@tanstack/vue-table": "^8.15.3",
"@tanstack/vue-table": "^8.16.0",
"@vee-validate/zod": "^4.12.6",
"@vueuse/core": "^10.9.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"h3": "^1.11.1",
"h3-clerk": "^0.3.9",
"iron-webcrypto": "^1.1.0",
"h3-clerk": "^0.3.10",
"iron-webcrypto": "^1.1.1",
"lucide-vue-next": "^0.365.0",
"nitropack": "2.9.6",
"radix-vue": "^1.6.2",
"tailwind-merge": "^2.2.2",
"qr-scanner-wechat": "^0.1.3",
"radix-vue": "^1.7.2",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"uncrypto": "^0.1.3",
"unstorage": "^1.10.2",
"uqr": "^0.1.2",
"uuid": "^9.0.1",
"v-calendar": "^3.1.2",
"vee-validate": "^4.12.6",
"vue-clerk": "^0.2.1",
"zod": "^3.22.4"
"vue-clerk": "^0.2.2",
"zod": "^3.23.4"
},
"devDependencies": {
"@antfu/eslint-config": "^2.12.2",
"@antfu/eslint-config": "^2.16.0",
"@nuxt/fonts": "^0.6.1",
"@nuxtjs/google-fonts": "^3.2.0",
"@nuxtjs/tailwindcss": "^6.11.4",
"@prisma/client": "^5.12.1",
"@nuxtjs/tailwindcss": "^6.12.0",
"@prisma/client": "^5.13.0",
"@rollup/plugin-wasm": "^6.2.2",
"@sentry/node": "^7.109.0",
"@sentry/profiling-node": "^7.109.0",
"@sentry/vue": "^7.109.0",
"@tailwindcss/typography": "^0.5.12",
"@sentry/node": "^7.112.2",
"@sentry/profiling-node": "^7.112.2",
"@sentry/vue": "^7.112.2",
"@tailwindcss/typography": "^0.5.13",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"eslint": "^8.57.0",
"nuxt": "3.11.2",
"nuxt-icon": "^0.6.10",
"nuxt-svgo": "^4.0.0",
"prisma": "^5.12.1",
"shadcn-nuxt": "^0.10.2",
"prisma": "^5.13.0",
"shadcn-nuxt": "^0.10.4",
"ts-node": "^10.9.2",
"tsx": "^4.7.2",
"typescript": "^5.4.4",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"vue-tsc": "^2.0.11"
"tsx": "^4.7.3",
"typescript": "^5.4.5",
"vue": "^3.4.25",
"vue-router": "^4.3.2",
"vue-tsc": "^2.0.14"
},
"prisma": {
"schema": "db/schema.prisma"
Expand Down
Loading