Skip to content

Commit b05462b

Browse files
committed
fix(posting): posting from modal
1 parent 07776d4 commit b05462b

File tree

10 files changed

+243
-262
lines changed

10 files changed

+243
-262
lines changed

app/components/Dropdown.vue

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<script setup lang="ts">
2+
/**
3+
* Legacy. Use new <InputDropdown/> instead
4+
*/
5+
26
import { onKeyStroke } from '@vueuse/core';
37
48
const emit = defineEmits<{

app/components/Input/Dropdown/MultiSelect.vue

Whitespace-only changes.

app/components/Input/Dropdown/SingleSelect.vue

Whitespace-only changes.

app/components/Input/Dropdown/index.vue

Whitespace-only changes.

app/components/admin/PostingCard.vue

+17-3
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,23 @@ if (props.posting && props.posting.tagsCSV) {
5858
Published
5959
</div>
6060
<div class="text-xs font-medium rounded-lg text-center px-2.5 py-1 bg-sky-100 text-sky-700" v-else>Draft</div>
61-
<InputButton as="NuxtLink" variant="outline" size="icon" :to="'/admin/postings/edit?id=' + posting.id"
62-
><Icon name="iconamoon:edit" class="w-5 h-5" />
63-
</InputButton>
61+
62+
<Modal class-override="p-4" :full-screen="true">
63+
<template #title-bar>
64+
<div class="flex items-center space-x-2">
65+
<Icon class="w-5 h-5 shrink-0 fill-current mr-2" name="iconamoon:edit" />
66+
<span>New Posting</span>
67+
</div>
68+
</template>
69+
<template #input="{ open }">
70+
<InputButton variant="outline" size="icon" @click.stop="open">
71+
<Icon name="iconamoon:edit" class="w-5 h-5" />
72+
</InputButton>
73+
</template>
74+
<template #content="{ close }">
75+
<LazyAdminPostingForm :id="posting.id" @done="close" />
76+
</template>
77+
</Modal>
6478
</div>
6579
</footer>
6680
</div>

app/components/admin/PostingForm.vue

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<script setup lang="ts">
2+
import type { CalendarDate } from '@internationalized/date';
3+
import { syncRef } from '@vueuse/core';
4+
import { employmentTypeIds } from '~~/shared/employment-types';
5+
import { createJobPostingSchema, updateJobPostingSchema } from '~~/shared/schemas/posting';
6+
7+
const props = defineProps<{
8+
id?: string;
9+
}>();
10+
11+
const emits = defineEmits<{
12+
done: [];
13+
}>();
14+
15+
const isEditing = !!props.id;
16+
17+
const { refresh } = await usePostingsRepository();
18+
19+
const isUpdating = ref(false);
20+
const isCreating = ref(false);
21+
const isExpired = ref(false);
22+
23+
const disableFields = computed(() => isUpdating.value || isCreating.value || isExpired.value);
24+
25+
const formSchema = toTypedSchema(isEditing ? updateJobPostingSchema : createJobPostingSchema);
26+
const { handleSubmit, errors, defineField } = useForm({
27+
validationSchema: formSchema,
28+
});
29+
30+
const validTillCalendarDate = shallowRef<CalendarDate>();
31+
32+
const [title] = defineField('title');
33+
const [contents] = defineField('contents');
34+
const [tagsCSV] = defineField('tagsCSV');
35+
const [isPublished] = defineField('isPublished');
36+
const [validTill] = defineField('validTill');
37+
const [isRemote] = defineField('isRemote');
38+
const [employmentType] = defineField('employmentType');
39+
40+
let onDelete = () => Promise.resolve();
41+
let onUpdate = () => Promise.resolve();
42+
let onCreate = handleSubmit(async (values) => {
43+
try {
44+
isCreating.value = true;
45+
await $fetch('/api/posting', {
46+
method: 'POST',
47+
body: {
48+
...values,
49+
},
50+
});
51+
refresh();
52+
emits('done');
53+
} catch (e) {
54+
console.error(e);
55+
} finally {
56+
isCreating.value = false;
57+
}
58+
});
59+
60+
if (isEditing) {
61+
// @ts-expect-error (only available when editing)
62+
const [id] = defineField('id');
63+
64+
const q = { id: props.id as string };
65+
const { data, changing, deleteData, updateData } = await usePostingRepository(q);
66+
67+
syncRef(changing, isUpdating, { direction: 'rtl' });
68+
69+
if (!data.value.id)
70+
throw createError({
71+
statusCode: 404,
72+
message: 'Unable to edit posting, not found.',
73+
});
74+
75+
id.value = data.value.id;
76+
title.value = data.value.title;
77+
contents.value = data.value.contents || undefined;
78+
tagsCSV.value = data.value.tagsCSV || undefined;
79+
isPublished.value = data.value.isPublished;
80+
validTill.value = (data.value.validTill as string | null) || undefined;
81+
isRemote.value = data.value.isRemote || false;
82+
employmentType.value = data.value.employmentType || employmentTypeIds[0];
83+
84+
// Initialise Extra Refs
85+
if (validTill.value) {
86+
validTillCalendarDate.value = dateToCalendarDate(new Date(validTill.value));
87+
}
88+
isExpired.value = data.value.isExpired;
89+
90+
onDelete = async () => {
91+
await deleteData(q);
92+
refresh();
93+
emits('done');
94+
};
95+
96+
onUpdate = handleSubmit(async (values) => {
97+
await updateData({ ...values, id: data.value.id });
98+
refresh();
99+
emits('done');
100+
}) as () => Promise<void>;
101+
} else {
102+
isRemote.value = false;
103+
employmentType.value = employmentTypeIds[0];
104+
}
105+
106+
const onSubmit = () => {
107+
isEditing ? onUpdate() : onCreate();
108+
};
109+
110+
watch(validTillCalendarDate, (calendarDate) => {
111+
if (!calendarDate) return;
112+
validTill.value = calendarDateToDate(calendarDate).toISOString();
113+
});
114+
</script>
115+
116+
<template>
117+
<div class="w-full max-w-8xl mx-auto">
118+
<div
119+
class="flex justify-center items-center border-b border-zinc-200 bg-red-200 text-red-600 text-sm py-1"
120+
v-if="isExpired"
121+
>
122+
Posting has expired. Editing is disabled.
123+
</div>
124+
<!-- Input Section -->
125+
<div class="flex w-full justify-end space-x-3 mt-2">
126+
<AbstractConfirmationBox
127+
title="Delete Posting?"
128+
content="You won't be able to undo this action. You will loose access to applicant list."
129+
@confirm="onDelete"
130+
v-if="isEditing"
131+
>
132+
<template #input="{ open }">
133+
<InputButton class="w-22" variant="destructive" @click="open" :disabled="disableFields">
134+
<div class="flex items-center space-x-1 w-full">
135+
<Icon name="material-symbols:delete-outline" class="h-4 w-4" />
136+
<span>Delete</span>
137+
</div>
138+
</InputButton>
139+
</template>
140+
</AbstractConfirmationBox>
141+
<AbstractConfirmationBox
142+
title="Save Posting?"
143+
content="Are you sure you want to save the changes?"
144+
@confirm="onSubmit"
145+
>
146+
<template #input="{ open }">
147+
<InputButton class="w-22" :disabled="disableFields" @click="open">
148+
<div class="flex items-center space-x-1 w-full">
149+
<Icon name="lets-icons:save" class="h-4 w-4" />
150+
<span>Save</span>
151+
</div>
152+
</InputButton>
153+
</template>
154+
</AbstractConfirmationBox>
155+
</div>
156+
<form @submit="onSubmit" class="w-full flex items-start py-2 space-x-6">
157+
<section class="flex flex-col w-2/3 space-y-3">
158+
<InputText
159+
placeholder="Senior Software Engineer"
160+
label="Title"
161+
:disabled="disableFields"
162+
:error="errors.title"
163+
v-model="title"
164+
/>
165+
<InputText
166+
placeholder="Remote, Full Time, San Fransisco"
167+
label="Tags (CSV)"
168+
:disabled="disableFields"
169+
v-model="tagsCSV"
170+
/>
171+
<InputLabel label="Job Description" :error="errors.contents">
172+
<template #input>
173+
<Editor
174+
placeholder="We are looking for someone who can..."
175+
editor-class="h-[480px] overflow-y-scroll"
176+
v-model="contents"
177+
:read-only="disableFields"
178+
/>
179+
</template>
180+
</InputLabel>
181+
</section>
182+
<div class="flex flex-col items-start justify-start space-y-3 w-1/3">
183+
<InputLabel label="Expiry Date">
184+
<template #input>
185+
<div class="border p-0.5 w-full rounded-lg">
186+
<InputDatePicker
187+
class="w-full"
188+
label="Expiry Date"
189+
v-model="validTillCalendarDate"
190+
:disabled="disableFields"
191+
/>
192+
</div>
193+
</template>
194+
</InputLabel>
195+
<InputLabel label="Publish?">
196+
<template #input>
197+
<div class="border p-[7px] w-full rounded-lg">
198+
<InputSwitch v-model="isPublished" :disabled="disableFields" />
199+
</div>
200+
</template>
201+
</InputLabel>
202+
</div>
203+
</form>
204+
</div>
205+
</template>

app/pages/admin/dashboard.vue

+17-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,23 @@ const { user: profile } = useUserSession();
1717
<div class="text-xl font-bold text-zinc-900 font-noto">👋🏽 Hello, {{ profile?.firstName }}</div>
1818
<div class="text-sm text-zinc-500">Track your hiring activities. You are almost there!</div>
1919
<div class="flex items-center mt-4">
20-
<InputButton as="NuxtLink" variant="secondary" to="/admin/postings/new">
21-
<span class="mr-2">Create Posting</span>
22-
<Icon name="ic:baseline-plus" class="w-4 h-4" />
23-
</InputButton>
20+
<Modal class-override="p-4" :full-screen="true">
21+
<template #title-bar>
22+
<div class="flex items-center space-x-2">
23+
<Icon class="w-5 h-5 shrink-0 fill-current mr-2" name="iconamoon:edit" />
24+
<span>New Posting</span>
25+
</div>
26+
</template>
27+
<template #input="{ open }">
28+
<InputButton variant="secondary" @click.stop="open">
29+
<span class="mr-2">Create Posting</span>
30+
<Icon name="ic:baseline-plus" class="w-4 h-4" />
31+
</InputButton>
32+
</template>
33+
<template #content="{ close }">
34+
<LazyAdminPostingForm @done="close" />
35+
</template>
36+
</Modal>
2437
</div>
2538
</section>
2639
<div class="mt-4">

app/pages/admin/postings/index.vue app/pages/admin/postings.vue

-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,3 @@
22
// Legacy redirect
33
await navigateTo('/admin/dashboard');
44
</script>
5-
6-
<template>
7-
<div></div>
8-
</template>

0 commit comments

Comments
 (0)