Skip to content

Commit c032d39

Browse files
authored
Merge pull request #204 from vuejs-jp/feature/admin-jobs
[ジョブボード] jobs (separate with sponsors) - publish test
2 parents 4f98489 + 211f3a5 commit c032d39

File tree

15 files changed

+423
-9
lines changed

15 files changed

+423
-9
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<script setup lang="ts">
2+
import type { Job, Sponsor } from '@vuejs-jp/model'
3+
import { ref } from 'vue'
4+
import { useSupabase } from '~/composables/useSupabase'
5+
import { useSupabaseStorage } from '~/composables/useSupabaseStorage'
6+
7+
interface AddJobProps {
8+
job?: Job
9+
sponsors: Sponsor[]
10+
}
11+
const emit = defineEmits<{ close: [] }>()
12+
const props = defineProps<AddJobProps>()
13+
14+
const { upsertJob, uploadAvatar } = useSupabase()
15+
const { getFullAvatarUrl } = useSupabaseStorage()
16+
17+
const newJob = ref({
18+
...props.job?.id && { id: props.job?.id },
19+
sponsor_id: props.job?.sponsor_id ?? '',
20+
link_url: props.job?.link_url ?? '',
21+
image_url: props.job?.image_url ?? '',
22+
image_alt: props.job?.image_alt ?? '',
23+
display_order: props.job?.display_order ?? null,
24+
is_open: props.job?.is_open ?? true,
25+
})
26+
27+
const updateDisplayOrder = (e: any) => {
28+
newJob.value.display_order = e.target.value
29+
}
30+
const updateLinkUrl = (e: any) => {
31+
newJob.value.link_url = e.target.value
32+
}
33+
const checkFiles = (files: File[]) => {
34+
if (files.length === 0) return
35+
36+
const file = files[0]
37+
// const filename = file.name
38+
const fileExt = file.name.split('.').pop()
39+
const filePath = `/${Math.random()}.${fileExt}`
40+
41+
uploadAvatar(filePath, file)
42+
43+
newJob.value.image_url = getFullAvatarUrl(filePath)
44+
}
45+
const updateImageAlt = (e: any) => {
46+
newJob.value.image_alt = e.target.value
47+
}
48+
49+
const onSubmit = () => {
50+
upsertJob('jobs', newJob.value)
51+
}
52+
</script>
53+
54+
<template>
55+
<div class="container">
56+
<VFTitle class="title">Job</VFTitle>
57+
<div class="form">
58+
<form @submit="onSubmit">
59+
<VFDropdownField
60+
id="sponsor_id"
61+
v-model="newJob.sponsor_id"
62+
name="sponsor_id"
63+
label="スポンサー"
64+
:items="sponsors.map((s) => ({ value: s.id, text: s.name }))"
65+
/>
66+
<VFInputField
67+
id="link_url"
68+
v-model="newJob.link_url"
69+
name="link_url"
70+
label="回遊させたい URL"
71+
@input="updateLinkUrl"
72+
/>
73+
<VFDragDropArea file-name="profiledata" file-accept="image/*" @check-files="checkFiles">
74+
<div class="upload">
75+
<img
76+
v-if="newJob.image_url"
77+
alt=""
78+
:src="newJob.image_url"
79+
height="60"
80+
decoding="async"
81+
/>
82+
<p>Drag & drop a file</p>
83+
<p>または</p>
84+
<p>Select a file</p>
85+
</div>
86+
</VFDragDropArea>
87+
<VFInputField
88+
id="image_alt"
89+
v-model="newJob.image_alt"
90+
name="image_alt"
91+
label="掲載画像の ALT 属性"
92+
@input="updateImageAlt"
93+
/>
94+
<VFInputField
95+
id="display_order"
96+
v-model="newJob.display_order"
97+
name="display_order"
98+
label="表示順"
99+
@input="updateDisplayOrder"
100+
/>
101+
<VFDropdownField
102+
id="is_open"
103+
v-model="newJob.is_open"
104+
name="is_open"
105+
label="表示・非表示"
106+
:items="[
107+
{ value: 'false', text: '非表示' },
108+
{ value: 'true', text: '表示' },
109+
]"
110+
/>
111+
<div class="form-button">
112+
<VFSubmitButton>Save</VFSubmitButton>
113+
<VFLinkButton
114+
is="button"
115+
class="action"
116+
background-color="white"
117+
color="vue-blue"
118+
@click="emit('close')"
119+
>
120+
Close
121+
</VFLinkButton>
122+
</div>
123+
</form>
124+
</div>
125+
</div>
126+
</template>
127+
128+
<style scoped>
129+
.container {
130+
height: 600px;
131+
overflow-y: scroll;
132+
}
133+
.form {
134+
padding: 40px 20px;
135+
}
136+
form {
137+
display: grid;
138+
gap: 40px;
139+
width: 100%;
140+
}
141+
.action {
142+
--height-button: 66px;
143+
144+
margin-top: 40px;
145+
height: var(--height-button);
146+
}
147+
@media (--tablet) {
148+
.action {
149+
--height-button: 49px;
150+
}
151+
}
152+
</style>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
interface JobListProps {
5+
jobs: any
6+
}
7+
8+
const emit = defineEmits<{ edit: [id: string] }>()
9+
10+
const props = defineProps<JobListProps>()
11+
12+
const showDialog = ref(false)
13+
const jobId = ref('')
14+
const handleDialog = (id?: string) => {
15+
showDialog.value = !showDialog.value
16+
jobId.value = id ?? ''
17+
}
18+
</script>
19+
20+
<template>
21+
<table id="jobs">
22+
<tr>
23+
<th>link_url</th>
24+
<th>image_url</th>
25+
<th>image_alt</th>
26+
<th>is_open</th>
27+
<th style="min-width: 80px">action</th>
28+
</tr>
29+
<tr v-for="job in jobs" :key="job.id">
30+
<td>{{ job.link_url }}</td>
31+
<td>
32+
<img
33+
v-if="job.image_url"
34+
alt=""
35+
:src="job.image_url"
36+
height="60"
37+
decoding="async"
38+
/>
39+
<p v-if="!job.image_url">
40+
No image
41+
</p>
42+
</td>
43+
<td>{{ job.image_alt }}</td>
44+
<td>
45+
<p>{{ job.is_open ? '表示' : '非表示' }}</p>
46+
<p v-if="job.display_order">{{ `(${job.display_order})` }}</p>
47+
</td>
48+
<td>
49+
<VFLinkButton
50+
is="button"
51+
class="action"
52+
background-color="white"
53+
color="vue-blue"
54+
@click="() => handleDialog(job?.id)"
55+
>
56+
Edit
57+
</VFLinkButton>
58+
</td>
59+
</tr>
60+
</table>
61+
<VFDialog v-if="showDialog">
62+
<AdminJobItem
63+
:job="jobs.filter((s) => s.id === jobId)[0]"
64+
@close="handleDialog"
65+
/>
66+
</VFDialog>
67+
</template>
68+
69+
<style scoped>
70+
#jobs {
71+
border-collapse: collapse;
72+
width: 100%;
73+
}
74+
75+
#jobs td,
76+
#jobs th {
77+
border: 1px solid #ddd;
78+
padding: 8px;
79+
}
80+
81+
#jobs tr:nth-child(even){
82+
background-color: #f2f2f2;
83+
}
84+
85+
#jobs tr:hover {
86+
background-color: #ddd;
87+
}
88+
89+
#jobs th {
90+
padding: 12px auto;
91+
text-align: left;
92+
background-color: var(--color-vue-green200);
93+
color: #fff;
94+
}
95+
</style>

apps/web/app/components/admin/Page.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ interface ListProps {
1414
const selectedRole = ref<Role>('attendee')
1515
1616
const { fetchData, fetchAttendeeData } = useSupabase()
17-
const { exportSpeaker, exportSponsor, exportAttendee, exportStaff } = useSupabaseCsv()
17+
const { exportSpeaker, exportSponsor, exportJob, exportAttendee, exportStaff } = useSupabaseCsv()
1818
const { write } = useCsv()
1919
const { data: speakers } = await useAsyncData('speakers', async () => {
2020
return await fetchData('speakers')
2121
})
2222
const { data: sponsors } = await useAsyncData('sponsors', async () => {
2323
return await fetchData('sponsors')
2424
})
25+
const { data: jobs } = await useAsyncData('jobs', async () => {
26+
return await fetchData('jobs')
27+
})
2528
const { data: attendees } = await useAsyncData('attendees', async () => {
2629
return await fetchAttendeeData('attendees', selectedRole.value)
2730
})
@@ -41,6 +44,7 @@ const handleCsv = async () => {
4144
const res = await match(props.page)
4245
.with('speaker', () => exportSpeaker('speakers'))
4346
.with('sponsor', () => exportSponsor('sponsors'))
47+
.with('job', () => exportJob('jobs'))
4448
.with('adminUser', () => exportStaff('staffs'))
4549
.with('namecard', () => exportAttendee('attendees'))
4650
.exhaustive()
@@ -91,6 +95,7 @@ const pageText = props.page.replace(/^[a-z]/g, function (val) {
9195
</div>
9296
<AdminSpeakerList v-if="page === 'speaker'" :speakers="speakers?.data" />
9397
<AdminSponsorList v-if="page === 'sponsor'" :sponsors="sponsors?.data" :speakers="speakers?.data" />
98+
<AdminJobList v-if="page === 'job'" :jobs="jobs?.data" :sponsors="sponsors?.data" />
9499
<div v-if="page === 'namecard'" class="tab-content-attendee">
95100
<VFDropdownField
96101
id="selected_role"
@@ -108,6 +113,7 @@ const pageText = props.page.replace(/^[a-z]/g, function (val) {
108113
<VFDialog v-if="showDialog">
109114
<AdminSpeakerItem v-if="page === 'speaker'" @close="handleDialog" />
110115
<AdminSponsorItem v-if="page === 'sponsor'" :speakers="speakers?.data" @close="handleDialog" />
116+
<AdminJobItem v-if="page === 'job'" :sponsors="sponsors?.data" @close="handleDialog" />
111117
<AdminStaffItem v-if="page === 'adminUser'" @close="handleDialog" />
112118
</VFDialog>
113119
</div>

apps/web/app/composables/useSupabase.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useSupabaseClient } from '#imports'
22
import { bucket, type Role, type Table } from '@vuejs-jp/model'
33
import type { Database } from '~/types/generated/supabase'
4-
import type { FormSpeaker, FormSponsor, FormAttendee, FormStaff } from '~/types/supabase'
4+
import type { FormSpeaker, FormSponsor, FormAttendee, FormStaff, FormJob } from '~/types/supabase'
55

66
export function useSupabase() {
77
const client = useSupabaseClient<Database>()
@@ -34,6 +34,13 @@ export function useSupabase() {
3434
if (error) return
3535
}
3636

37+
async function upsertJob(table: Extract<Table, 'jobs'>, target: FormJob) {
38+
const targetData = { ...target }
39+
40+
const { error } = await client.from(table).upsert(targetData)
41+
if (error) return
42+
}
43+
3744
async function upsertAttendee(table: Extract<Table, 'attendees'>, target: FormAttendee) {
3845
const targetData = { ...target }
3946

@@ -52,5 +59,14 @@ export function useSupabase() {
5259
await client.storage.from(bucket).upload(filePath, file)
5360
}
5461

55-
return { fetchData, fetchAttendeeData, upsertSpeaker, upsertSponsor, upsertAttendee, upsertStaff, uploadAvatar }
62+
return {
63+
fetchData,
64+
fetchAttendeeData,
65+
upsertSpeaker,
66+
upsertSponsor,
67+
upsertJob,
68+
upsertAttendee,
69+
upsertStaff,
70+
uploadAvatar,
71+
}
5672
}

apps/web/app/composables/useSupabaseCsv.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ export function useSupabaseCsv() {
2525
return data
2626
}
2727

28+
async function exportJob(table: Extract<Table, 'jobs'>) {
29+
const { data, error } = await client.from(table)
30+
.select()
31+
.eq('is_open', true)
32+
.csv()
33+
if (error) return
34+
35+
return data
36+
}
37+
2838
async function exportAttendee(table: Extract<Table, 'attendees'>) {
2939
const { data, error } = await client.from(table)
3040
.select('display_name, image_url, image_file_name')
@@ -45,5 +55,5 @@ export function useSupabaseCsv() {
4555
return data
4656
}
4757

48-
return { exportSpeaker, exportSponsor, exportAttendee, exportStaff }
58+
return { exportSpeaker, exportSponsor, exportJob, exportAttendee, exportStaff }
4959
}

apps/web/app/pages/jobboard.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
<script setup lang="ts">
2-
import { useHead } from '#imports'
2+
import { useFetch, useHead } from '#imports'
3+
import type { JobInfo } from '@vuejs-jp/model'
34
import { conferenceTitle, linkUrl, ogJobboardDescription } from '~/utils/constants'
45
import { generalOg, twitterOg } from '~/utils/og.constants'
56
7+
type Jobs = Record<'allJobs', JobInfo>
8+
9+
const { data, error } = await useFetch('/api/jobs')
10+
if (error.value) {
11+
console.error(error.value)
12+
}
13+
const { allJobs } = data.value as Jobs
614
715
useHead({
816
titleTemplate: (titleChunk) => `ジョブボード | ${conferenceTitle}`,
@@ -25,8 +33,10 @@ useHead({
2533
<VFPageHeading>{{ $t('jobboard.title') }}</VFPageHeading>
2634
<div class="jobboard">
2735
<ul class="jobboard-body">
28-
<li v-for="i in 7">
29-
<nuxt-link to="hoge" target="_blank"><img src="https://placehold.jp/f0f0f0/ffffff/920x520.png?text=%20" alt="" /></nuxt-link>
36+
<li v-for="(job, index) in allJobs.list" :key="index">
37+
<nuxt-link :to="job.link_url" target="_blank">
38+
<img :src="job.image_url" :alt="job.image_alt" />
39+
</nuxt-link>
3040
</li>
3141
</ul>
3242
</div>

apps/web/app/pages/staff/console.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,12 @@ const { hasAuth } = useAuthSession()
5454
<AdminPage page="sponsor" />
5555
</template>
5656
<template #tab_content_2>
57-
<AdminPage page="namecard" />
57+
<AdminPage page="job" />
5858
</template>
5959
<template #tab_content_3>
60+
<AdminPage page="namecard" />
61+
</template>
62+
<template #tab_content_4>
6063
<AdminPage page="adminUser" />
6164
</template>
6265
</VFTab>

0 commit comments

Comments
 (0)