Skip to content

Commit

Permalink
feat(posting): date-picker for expiry
Browse files Browse the repository at this point in the history
  • Loading branch information
amandesai01 committed Oct 22, 2024
1 parent 1166920 commit 02e16a9
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 29 deletions.
133 changes: 133 additions & 0 deletions app/components/Input/DatePicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import {
DatePickerArrow,
DatePickerCalendar,
DatePickerCell,
DatePickerCellTrigger,
DatePickerContent,
DatePickerField,
DatePickerGrid,
DatePickerGridBody,
DatePickerGridHead,
DatePickerGridRow,
DatePickerHeadCell,
DatePickerHeader,
DatePickerHeading,
DatePickerNext,
DatePickerPrev,
DatePickerRoot,
DatePickerTrigger,
Label,
} from 'radix-vue';
import { CalendarDate } from '@internationalized/date';
import { useVModel } from '@vueuse/core';
const props = withDefaults(
defineProps<{
modelValue?: CalendarDate;
label?: string;
}>(),
{
label: 'Pick a date',
}
);
const emits = defineEmits<{
'update:modelValue': [CalendarDate];
}>();
const selectedDate = useVModel(props, 'modelValue', emits);
const todaysDate = useTodaysDate();
const isDateDisabled = (date: CalendarDate) => date.compare(todaysDate) <= 0;
const open = ref<boolean>();
const defaultDate = selectedDate.value ? selectedDate.value : todaysDate.add({ days: 8 });
const dateModel = ref<CalendarDate>(defaultDate);
const onSelect = () => {
open.value = false;
// @ts-expect-error
selectedDate.value = dateModel.value;
};
</script>

<template>
<div class="flex items-center border p-1 rounded-lg space-x-2">
<!-- @vue-expect-error -->
<DatePickerRoot id="date-field" :is-date-disabled v-model:open="open" v-model:model-value="dateModel">
<DatePickerField class="flex items-center space-x-2 text-zinc-800 border border-transparent">
<div class="flex flex-col">
<Label class="text-sm text-zinc-800 font-bold" for="date-field">{{ props.label }}</Label>
<span class="text-xs text-zinc-400" v-if="selectedDate">{{
`${selectedDate.month}/${selectedDate.day}/${selectedDate.year}`
}}</span>
</div>
<DatePickerTrigger
class="rounded-md text-lg focus:shadow-black flex items-center hover:bg-zinc-200 p-1 border"
@click="open = !open"
>
<Icon icon="radix-icons:calendar" class="w-5 h-5" />
</DatePickerTrigger>
</DatePickerField>

<DatePickerContent
:side-offset="2"
class="border shadow-md rounded-xl bg-white data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
>
<DatePickerArrow class="fill-white" />
<DatePickerCalendar v-slot="{ weekDays, grid }" class="p-4">
<DatePickerHeader class="flex items-center justify-between">
<DatePickerPrev
class="text-zinc-400 justify-center rounded-lg w-8 h-8 hover:border inline-flex items-center"
>
<Icon icon="radix-icons:chevron-left" class="w-6 h-6" />
</DatePickerPrev>

<DatePickerHeading class="text-black font-bold font-lato" />
<DatePickerNext
class="text-zinc-400 justify-center rounded-lg w-8 h-8 hover:border inline-flex items-center"
>
<Icon icon="radix-icons:chevron-right" class="w-6 h-6" />
</DatePickerNext>
</DatePickerHeader>
<div class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0">
<DatePickerGrid
v-for="month in grid"
:key="month.value.toString()"
class="w-full border-collapse select-none space-y-1"
>
<DatePickerGridHead>
<DatePickerGridRow class="mb-1 flex w-full justify-between">
<DatePickerHeadCell v-for="day in weekDays" :key="day" class="w-8 rounded-md text-xs text-green8">
{{ day }}
</DatePickerHeadCell>
</DatePickerGridRow>
</DatePickerGridHead>
<DatePickerGridBody>
<DatePickerGridRow
v-for="(weekDates, index) in month.rows"
:key="`weekDate-${index}`"
class="flex w-full"
>
<DatePickerCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
<DatePickerCellTrigger
:day="weekDate"
:month="month.value"
class="relative flex items-center justify-center whitespace-nowrap rounded-lg border text-sm font-normal text-zinc-700 w-8 h-8 outline-none hover:border-zinc-300 data-[selected]:bg-zinc-800 data-[selected]:font-medium data-[disabled]:text-zinc-300 data-[selected]:text-white data-[unavailable]:pointer-events-none data-[unavailable]:text-black/30 data-[unavailable]:line-through before:absolute before:top-[5px] before:hidden before:rounded-full before:w-1 before:h-1 before:bg-white data-[today]:before:block data-[today]:before:bg-green9 data-[selected]:before:bg-white"
/>
</DatePickerCell>
</DatePickerGridRow>
</DatePickerGridBody>
</DatePickerGrid>
</div>
</DatePickerCalendar>
<div class="flex justify-between items-center text-sm text-zinc-500 mb-2 px-4">
<span>{{ `${dateModel.month}/${dateModel.day}/${dateModel.year}` }}</span>
<InputButton :size="'sm'" @click.prevent="onSelect">Select</InputButton>
</div>
</DatePickerContent>
</DatePickerRoot>
</div>
</template>
17 changes: 17 additions & 0 deletions app/composables/utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CalendarDate } from '@internationalized/date';

export function useTodaysDate() {
const todaysDate = new Date();
const todaysDateCalendar = dateToCalendarDate(todaysDate);

return todaysDateCalendar;
}

export function dateToCalendarDate(date: Date) {
const calendarDate = new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
return calendarDate;
}

export function calendarDateToDate(calendarDate: CalendarDate) {
return new Date(calendarDate.toString());
}
18 changes: 18 additions & 0 deletions app/pages/admin/postings/edit.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { CalendarDate } from '@internationalized/date';
import { updateJobPostingSchema } from '~~/shared/schemas/posting';
definePageMeta({
Expand Down Expand Up @@ -26,18 +27,29 @@ const { handleSubmit, errors, defineField } = useForm({
validationSchema: formSchema,
});
const validTillCalendarDate = ref<CalendarDate>();
const [id] = defineField('id');
// Declare form fields
const [title] = defineField('title');
const [contents] = defineField('contents');
const [tagsCSV] = defineField('tagsCSV');
const [isPublished] = defineField('isPublished');
const [validTill] = defineField('validTill');
// Initialise form fields
id.value = data.value.id;
title.value = data.value.title;
contents.value = data.value.contents || undefined;
tagsCSV.value = data.value.tagsCSV || undefined;
isPublished.value = data.value.isPublished;
validTill.value = (data.value.validTill as string | null) || undefined;
// Initialise Extra Refs
if (validTill.value) {
validTillCalendarDate.value = dateToCalendarDate(new Date(validTill.value));
}
const onSubmit = handleSubmit(async (values) => {
await updateData(values);
Expand All @@ -50,6 +62,11 @@ const onDelete = async () => {
refresh();
await navigateTo('/admin/postings');
};
watch(validTillCalendarDate, (calendarDate) => {
if (!calendarDate) return;
validTill.value = calendarDateToDate(calendarDate).toISOString();
});
</script>

<template>
Expand All @@ -65,6 +82,7 @@ const onDelete = async () => {
</div>
<!-- Right: Actions -->
<div class="flex items-center space-x-3">
<InputDatePicker label="Expiry Date" v-model="validTillCalendarDate" />
<AbstractConfirmationBox
title="Delete Posting?"
content="You won't be able to undo this action. You will loose access to applicant list."
Expand Down
3 changes: 3 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export default defineNuxtConfig({
},
useStylesheet: true,
},
imports: {
dirs: ['composables/**/*.ts'],
},
devtools: { enabled: true },
/**
* Sensible defaults, overriden by env vars.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"prepare": "husky"
},
"dependencies": {
"@internationalized/date": "^3.5.6",
"@nuxtjs/google-fonts": "^3.2.0",
"@nuxtjs/tailwindcss": "6.12.1",
"@profilecity/unstorage-s3-driver": "^0.0.3",
Expand Down
3 changes: 2 additions & 1 deletion server/api/posting/index.put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ export default defineEventHandler(async (event) => {
});
}

const updateQuery = {
const updateQuery: typeof jobPostingsTable.$inferInsert = {
...q,
updatedAt: new Date(),
validTill: q.validTill ? new Date(q.validTill) : undefined,
};

const updatedJobPosting = (
Expand Down
2 changes: 2 additions & 0 deletions shared/schemas/posting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const createJobPostingSchema = z.object({
contents: z.string().optional(),
tagsCSV: z.string().optional(),
isPublished: z.boolean(),
validTill: z.string().optional(), // Ideally it is a date field, but gets converted to string due to stringification.
});

export const updateJobPostingSchema = z.object({
Expand All @@ -24,6 +25,7 @@ export const updateJobPostingSchema = z.object({
contents: z.string().optional(),
tagsCSV: z.string().optional(),
isPublished: z.boolean(),
validTill: z.string().optional(), // Ideally it is a date field, but gets converted to string due to stringification.
});

export const deleteJobPostingSchema = z.object({
Expand Down
38 changes: 10 additions & 28 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,13 @@
dependencies:
"@swc/helpers" "^0.5.0"

"@internationalized/date@^3.5.6":
version "3.5.6"
resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.5.6.tgz#0833c2fa75efb3573f4e3bf10e3895f1019e87dd"
integrity sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==
dependencies:
"@swc/helpers" "^0.5.0"

"@internationalized/number@^3.5.3":
version "3.5.3"
resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.5.3.tgz#9fa060c1c4809f23fb3d38dd3f3d1ae4c87e95a8"
Expand Down Expand Up @@ -6496,16 +6503,7 @@ string-argv@~0.3.2:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6"
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==

"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -6546,14 +6544,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand Down Expand Up @@ -7280,16 +7271,7 @@ wide-align@^1.1.2:
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand Down

0 comments on commit 02e16a9

Please sign in to comment.