Skip to content

Commit

Permalink
feat (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
jiehousekeeper authored Sep 5, 2024
2 parents a781709 + 4214339 commit f06f303
Show file tree
Hide file tree
Showing 175 changed files with 7,705 additions and 5,107 deletions.
2 changes: 2 additions & 0 deletions .vscode/.cspell/name.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ alanlu
Anson
antfu
dbaeumer
ecpay
esbenp
hypernym
jiehousekeeper
zadigetvoltaire
2 changes: 2 additions & 0 deletions .vscode/.cspell/other.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Custom Dictionary Words
astrisks
BNPL
Fira
llen
lrange
Expand All @@ -8,6 +9,7 @@ noobserver
Noto
preinstall
rpush
TWQR
typecheck
wght
zod's
1 change: 1 addition & 0 deletions .vscode/.cspell/repo.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ tsup
Turborepo
unplugin
unstorage
vercel
videojs
vueuse
52 changes: 29 additions & 23 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
VERCEL_ENV=preview
NUXT_PUBLIC_SITE_NAME=中華民國職業清潔認證協會
NUXT_PUBLIC_SITE_URL=https://official-site-web-git-dev-jiehousekeepers-projects.vercel.app
NUXT_PUBLIC_GTAG_ID=G-XXXXXXXXXX
NUXT_PUBLIC_GTM_ID=GTM-xxxxxxxx
NUXT_PUBLIC_FB_URL=https://www.facebook.com/facebook
VERCEL_ENV=preview
VERCEL_BYPASS_TOKEN=f581938b887377b0e75a6c2f8425614f
EDGE_CONFIG=https://edge-config.vercel.com/XXXX?token=XXX-XXX-XXX-XXX
KV_URL=redis://...:6379
KV_REST_API_URL=...
KV_REST_API_TOKEN=...
REDIS_HOST=redis-....
REDIS_PORT=11070
REDIS_PASSWORD=...
STORAGE_TYPE=redis
NOTION_API_KEY=secret_...
NOTION_DATABASE_ID_GALLERIES=...
NOTION_DATABASE_ID_NEWS=...
NOTION_DATABASE_ID_INSTRUCTORS=...
NOTION_DATABASE_ID_CLASSROOMS=...
NOTION_DATABASE_ID_COURSES=...
NOTION_DATABASE_ID_COURSE_EVENTS=...
NOTION_DATABASE_ID_REVIEWS=...
NOTION_DATABASE_ID_FAQ=...
NOTION_DATABASE_ID_CONTACTS=...
NOTION_DATABASE_ID_MEMBERS=...
NOTION_DATABASE_ID_ORDERS==...
ECPAY_MERCHANT_ID=3002607
ECPAY_HASH_KEY=pwFHCqoQZGmho4w6
ECPAY_HASH_IV=EkRm7iFT261dpevs
ECPAY_STAGE=0
NUXT_REDIS_HOST=redis-....
NUXT_REDIS_PORT=11070
NUXT_REDIS_PASSWORD=...
NUXT_STORAGE_TYPE=redis
NUXT_NOTION_API_KEY=secret_...
NUXT_NOTION_DATABASE_ID_GALLERIES=...
NUXT_NOTION_DATABASE_ID_NEWS=...
NUXT_NOTION_DATABASE_ID_INSTRUCTORS=...
NUXT_NOTION_DATABASE_ID_CLASSROOMS=...
NUXT_NOTION_DATABASE_ID_COURSES=...
NUXT_NOTION_DATABASE_ID_COURSE_BASES=...
NUXT_NOTION_DATABASE_ID_COURSE_EVENTS=...
NUXT_NOTION_DATABASE_ID_REVIEWS=...
NUXT_NOTION_DATABASE_ID_FAQ=...
NUXT_NOTION_DATABASE_ID_PARTNERS=...
NUXT_NOTION_DATABASE_ID_CONTACTS=...
NUXT_NOTION_DATABASE_ID_MEMBERS=...
NUXT_NOTION_DATABASE_ID_ORDERS==...
NUXT_NOTION_DATABASE_ID_META=...
NUXT_NOTION_DATABASE_ID_MONTHS=...
NUXT_ECPAY_MERCHANT_ID=3002607
NUXT_ECPAY_HASH_KEY=pwFHCqoQZGmho4w6
NUXT_ECPAY_HASH_IV=EkRm7iFT261dpevs
NUXT_ECPAY_STAGE=1
NUXT_NODEMAILER_FROM_NAME=中華民國職業清潔認證協會
NUXT_NODEMAILER_FROM_MAIL=[email protected]
NUXT_NODEMAILER_SERVICE=SendGrid
NUXT_NODEMAILER_AUTH_USER=apikey
NUXT_NODEMAILER_AUTH_PASS=SG....
NUXT_PUBLIC_GTAG_ID=G-XXXXXXXXXX
NUXT_PUBLIC_GTM_ID=GTM-xxxxxxxx
COMPATIBILITY_DATE=2024-07-20
1 change: 1 addition & 0 deletions apps/web/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default defineAppConfig({})
57 changes: 54 additions & 3 deletions apps/web/app.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,66 @@
<script lang="ts" setup>
import { ModalsContainer } from 'vue-final-modal'
const siteConfig = useSiteConfig()
const config = useRuntimeConfig()
const route = useRoute()
const { $refreshAos } = useNuxtApp()
useHead({
htmlAttrs: {
id: 'mcss',
lang: siteConfig.defaultLocale,
},
bodyAttrs: {
class: 'normal scrollbar',
class: 'normal scrollbar {top:59}_.Toastify__toast-container {top:68}_.Toastify__toast-container@tablet {top:74}_.Toastify__toast-container@lg',
},
link: [{ rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
titleTemplate: '%s',
templateParams: {
schemaOrg: {
host: siteConfig.url,
inLanguage: siteConfig.defaultLocale, // locale.value, refs are supported
},
},
})
const fullPath = computed(() => (route.name === 'index' ? '/index' : route.fullPath))
const metaStore = useMetaStore()
await callOnce(async () => {
console.log('callOnce', fullPath.value, config.public.isDev, import.meta.server, config.vercel?.bypassToken)
await metaStore.updateMeta(
fullPath.value,
null,
!config.public.isDev && import.meta.server
? {
headers: {
'x-prerender-revalidate': config.vercel.bypassToken,
'x-ssr-cache': true,
},
}
: {},
)
})
metaStore.updateMeta(fullPath.value)
onMounted(() => {
watch(fullPath, async (path) => {
$refreshAos()
switch (route.name) {
case 'index':
case 'course':
case 'instructor':
case 'review':
case 'faq':
case 'news':
case 'about': {
metaStore.updateMeta(path)
}
}
})
})
const CSSRuntimeProvider = defineAsyncComponent(async () => (await import('@master/css.vue')).CSSRuntimeProvider)
Expand All @@ -19,7 +71,6 @@ const CSSRuntimeProvider = defineAsyncComponent(async () => (await import('@mast
<NuxtLayout>
<NuxtLoadingIndicator />
<NuxtPage />
<ModalsContainer />
</NuxtLayout>
</CSSRuntimeProvider>
</template>
22 changes: 0 additions & 22 deletions apps/web/components/About/AboutCard.vue

This file was deleted.

29 changes: 20 additions & 9 deletions apps/web/components/About/AboutContact.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
<script setup lang="ts">
import { createZodPlugin } from '@formkit/zod'
import { reset } from '@formkit/core'
import { ContactSchema } from '~/schema/contact'
import type { ContactSchemaType } from '~/schema/contact'
const show = ref(false)
const data = ref<ContactSchemaType>()
const contactData = ref<ContactSchemaType>()
const [zodPlugin, submitHandler] = createZodPlugin(ContactSchema, async (formData) => {
// await new Promise((r) => setTimeout(r, 2000))
const result = await $fetch('/api/contact', { method: 'post', body: formData })
console.log(result)
show.value = true
const { error } = await useApiFetch('/api/contact', { method: 'post', body: formData })
if (!error.value) {
show.value = true
// const { title, message, ...initData } = formData
// reset('contactForm', initData)
reset('contactForm')
}
})
</script>

Expand All @@ -23,20 +29,25 @@ const [zodPlugin, submitHandler] = createZodPlugin(ContactSchema, async (formDat
</div>

<div class="{mt:4x;text:right}_.formkit-actions mt:5x">
<FormKit v-model="data" type="form" :actions="true" submit-label="送出" :plugins="[zodPlugin]" @submit="submitHandler">
<FormKit id="contactForm" v-model="contactData" type="form" :config="{ validationVisibility: 'submit' }" :actions="true" submit-label="送出" :plugins="[zodPlugin]" @submit="submitHandler">
<div class="{grid-cols:1;gap:4x|6x} {grid-cols:2}@tablet">
<FormKit type="text" name="name" label="姓名" validation="required" />
<FormKit type="text" name="mobile" label="聯絡電話" validation="required|phone" />
<FormKit type="text" name="mobile" label="手機" validation="required|phone" />
<FormKit type="email" name="email" label="電子信箱" validation="required|email" />
<FormKit type="text" name="title" label="主旨" validation="required" />
</div>
<FormKit :classes="{ wrapper: 'mt:4x!' }" type="textarea" name="message" validation="required" label="問題描述,寫下您的問題" lines="3" />
</FormKit>
</div>

<Modal v-model="show" title="成功送出!" @confirm="() => (show = false)">
<p>已收到您的留言,</p>
<p>我們將盡快與您聯絡。</p>
<Modal v-model="show" :footer="null">
<template #header>
<h2 class="h2 fg:font-title">成功送出!</h2>
</template>
<div class="b1-r text:center">
<p>已收到您的留言,</p>
<p>我們將盡快與您聯絡。</p>
</div>
</Modal>
</section>
</template>
29 changes: 29 additions & 0 deletions apps/web/components/About/AboutPartners.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { PartnerSchemaType } from '~/schema/partner'
defineProps<{
partner: PartnerSchemaType
}>()
</script>

<template>
<div class="bg:home p:6x|12x r:2x">
<div class="{flex;flex:col;gap:5x} {flex:row}@2xs">
<div class="flex-basis:200@2xs">
<template v-if="partner.連結!">
<a :href="partner.連結!" class="block aspect:1/1 mx:auto overflow:hidden r:2x w:80% w:full@2xs">
<Image class="{aspect:inherit;object:cover}" :src="partner.圖片[0]" :alt="partner.圖片alt || partner.名稱" />
</a>
</template>
<template v-else>
<div class="aspect:1/1 mx:auto overflow:hidden r:2x w:80% w:full@2xs">
<Image class="{aspect:inherit;object:cover}" :src="partner.圖片[0]" :alt="partner.圖片alt || partner.名稱" />
</div>
</template>
</div>
<div class="b1-r {flex;ai:center} flex:1">
<p v-for="(p, idx) in partner.介紹?.split('\n')" :key="`${partner.名稱}-${idx}`">{{ p }}</p>
</div>
</div>
</div>
</template>
15 changes: 14 additions & 1 deletion apps/web/components/Breadcrumb.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
<script setup lang="ts">
interface IProps {
title?: string
isPageRoot?: boolean
noLastPage?: boolean
}
const props = defineProps<IProps>()
const links = useBreadcrumbItems()
const links = useBreadcrumbItems({
overrides: !props.title
? []
: [
undefined, // Home
undefined, // Blog
props.isPageRoot ? false : undefined,
{
label: props.title,
},
],
schemaOrg: true,
})
const processLinks = computed(() => {
if (props.noLastPage) {
Expand Down
23 changes: 13 additions & 10 deletions apps/web/components/Course/CourseCard.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
<script setup lang="ts">
import { formatThousand } from '@alanlu-dev/utils'
import type { CourseEventSchemaType } from '~/schema/course_event'
import type { CourseSchemaType } from '~/schema/course'
defineProps<IProps>()
const config = useRuntimeConfig()
interface IProps {
event: CourseEventSchemaType
course: CourseSchemaType
}
defineProps<IProps>()
</script>

<template>
<nuxt-link class="{flex;flex:row} {flex:col}@tablet bg:base-bg overflow:hidden r:2x@tablet scale(1.05):hover_img shadow:md@tablet" :to="`/course_event/${event.ID}`">
<nuxt-link class="{flex;flex:row} {flex:col}@tablet bg:base-bg overflow:hidden r:2x@tablet scale(1.05):hover_img shadow:md@tablet" :to="`/course/${course.ID}`">
<div class="{min-w:40%;pt:2x} {min-w:unset;pt:unset}@tablet">
<div class="rel aspect:147/102 aspect:316/133@tablet overflow:hidden r:2x r:unset@tablet">
<nuxt-img class="{abs;inset:0;full} ~300ms|ease object:cover" :src="event.課程資訊?.課程照片?.[0]" :alt="event.課程資訊?.課程名稱" />
<div class="rel aspect:16/9 cursor:pointer_img overflow:hidden r:2x r:unset@tablet">
<VideoPlayerCover aspect="16/9" class="{abs;inset:0;full} ~300ms|ease_img" :img="course.課程照片?.[0]" :alt="course.照片alt?.[0] || course.名稱" />
</div>
</div>

<div class="p:2x|4x text:left">
<p class="b1-b fg:primary lines:1">{{ event.課程資訊?.課程名稱 }} | {{ event.教室資訊?.名稱 }}</p>
<p class="b2-r lines:2 lines:3@tablet mt:2x">{{ event.課程資訊?.課程特色資訊?.map((i) => i?.課程特色).join('、') }}</p>
<p class="b2-r f:12@<=mobile fg:font-title lines:1 mt:3x mt:8x@tablet">中華民國職業清潔認証協會</p>
<p class="b2-m fg:primary lines:2">{{ course.名稱 }}</p>
<p class="b2-r lines:2 lines:3@tablet mt:2x">{{ course.課程基礎資訊?.課程特色.join('、') }}</p>
<p class="b2-r f:3x!@<=tablet fg:font-title lines:1 mt:3x mt:8x@tablet">{{ config.public.siteName }}</p>
<hr class="bg:#DBD9D9 h:1 mt:2x" />
<div class="mt:2x text:left">
<p class="h3 fg:accent!">NT$ {{ formatThousand(event.指定價格 || event.課程資訊?.價格 || 99999) }} </p>
<p class="h3 fg:accent!">NT$ {{ formatThousand(course.價格) }} </p>
</div>
</div>
</nuxt-link>
Expand Down
43 changes: 43 additions & 0 deletions apps/web/components/Course/CourseInstructorCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
import type { InstructorSchemaType } from '~/schema/instructor'
interface IProps {
instructor: InstructorSchemaType
}
defineProps<IProps>()
</script>

<template>
<div class="{block;content:'';h:1;bg:divider;w:full;my:5x}::after {my:10x}::after@xs {hidden}:last::after my:5x">
<div class="{flex;flex:col;ai:flex-start;jc:flex-start;gap:5x;flex:wrap} {flex:row}@desktop mt:5x mt:10x@tablet">
<div class="flex:1 min-w:100% min-w:40%@desktop order:2@desktop overflow:hidden r:2x">
<div class="rel aspect:4/3">
<VideoPlayerCover aspect="4/3" class="{abs;inset:0;full} r:2x" :img="instructor.照片[0]" :alt="instructor.名稱" />
</div>
</div>
<div class="flex:2">
<h3 class="h3">{{ instructor.名稱 }} {{ instructor.英文名 }}</h3>
<p class="b1-m mt:1x">{{ instructor.頭銜 }}</p>
<div class="list b1-r mt:2x mt:3x@tablet pl:0.5x@tablet">
<ul class="fg:font-title mt:4x">
<li>{{ instructor.工作經驗 }} 年收納工作經驗</li>
<li>{{ instructor.教學經驗 }} 年教學經驗</li>
<li>{{ instructor.標語 }}</li>
<li>
<p>專業認證:</p>
<ul class="list-style-type:circle!">
<li v-for="(item, idx) in instructor.專業認證" :key="`專業認證-${idx}`">{{ item }}</li>
</ul>
</li>
<li>
<p>服務經驗:</p>
<ul class="list-style-type:circle!">
<li>{{ instructor.服務經驗 }}</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
Loading

0 comments on commit f06f303

Please sign in to comment.