@careflair/common 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@ export { WorkHistoryInputValidation, WorkHistorySchema, } from "./schemas/workHi
4
4
  export { AvailabilitiesSchemaZod, AvailabilityZodInput, } from "./schemas/availabilitySchemaValidation";
5
5
  export { HourlyRateInputZod, HourlyRateInputZodType, } from "./schemas/hourlyRateSchemaValidation";
6
6
  export { ServicesSchema, validateServices, } from "./schemas/businessServicesValidation";
7
- export { detectVideoProvider, isValidCommunityVideoUrl, isValidYouTubeOrVimeoUrl, validateVideoLink, VideoProvider, VideoValidationResult, } from "./utils/videoValidation";
7
+ export { VideoProvider, VideoValidationResult, detectVideoProvider, isValidCommunityVideoUrl, isValidYouTubeOrVimeoUrl, validateVideoLink, } from "./utils/videoValidation";
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,210 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Registration form schema
4
+ */
5
+ export declare const registrationFormSchema: z.ZodEffects<z.ZodObject<{
6
+ firstName: z.ZodString;
7
+ lastName: z.ZodString;
8
+ email: z.ZodString;
9
+ username: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>;
10
+ role: z.ZodEffects<z.ZodString, string, string>;
11
+ password: z.ZodString;
12
+ confirmPassword: z.ZodString;
13
+ fingerPrint: z.ZodOptional<z.ZodString>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ role: string;
16
+ username: string;
17
+ email: string;
18
+ password: string;
19
+ firstName: string;
20
+ lastName: string;
21
+ confirmPassword: string;
22
+ fingerPrint?: string | undefined;
23
+ }, {
24
+ role: string;
25
+ username: string;
26
+ email: string;
27
+ password: string;
28
+ firstName: string;
29
+ lastName: string;
30
+ confirmPassword: string;
31
+ fingerPrint?: string | undefined;
32
+ }>, {
33
+ role: string;
34
+ username: string;
35
+ email: string;
36
+ password: string;
37
+ firstName: string;
38
+ lastName: string;
39
+ confirmPassword: string;
40
+ fingerPrint?: string | undefined;
41
+ }, {
42
+ role: string;
43
+ username: string;
44
+ email: string;
45
+ password: string;
46
+ firstName: string;
47
+ lastName: string;
48
+ confirmPassword: string;
49
+ fingerPrint?: string | undefined;
50
+ }>;
51
+ /**
52
+ * Participant location form schema
53
+ */
54
+ export declare const participantLocationFormSchema: z.ZodObject<{
55
+ address: z.ZodEffects<z.ZodObject<{
56
+ value: z.ZodString;
57
+ label: z.ZodString;
58
+ }, "strip", z.ZodTypeAny, {
59
+ value: string;
60
+ label: string;
61
+ }, {
62
+ value: string;
63
+ label: string;
64
+ }>, {
65
+ value: string;
66
+ label: string;
67
+ }, {
68
+ value: string;
69
+ label: string;
70
+ }>;
71
+ state: z.ZodEffects<z.ZodObject<{
72
+ value: z.ZodString;
73
+ label: z.ZodString;
74
+ }, "strip", z.ZodTypeAny, {
75
+ value: string;
76
+ label: string;
77
+ }, {
78
+ value: string;
79
+ label: string;
80
+ }>, {
81
+ value: string;
82
+ label: string;
83
+ }, {
84
+ value: string;
85
+ label: string;
86
+ }>;
87
+ }, "strip", z.ZodTypeAny, {
88
+ state: {
89
+ value: string;
90
+ label: string;
91
+ };
92
+ address: {
93
+ value: string;
94
+ label: string;
95
+ };
96
+ }, {
97
+ state: {
98
+ value: string;
99
+ label: string;
100
+ };
101
+ address: {
102
+ value: string;
103
+ label: string;
104
+ };
105
+ }>;
106
+ /**
107
+ * Contact details form schema
108
+ */
109
+ export declare const ContactDetailsFormSchema: z.ZodObject<{
110
+ phone: z.ZodUnion<[z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>, z.ZodLiteral<"">]>;
111
+ email: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
112
+ website: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
113
+ enableSmsNotifications: z.ZodOptional<z.ZodBoolean>;
114
+ }, "strip", z.ZodTypeAny, {
115
+ email?: string | undefined;
116
+ phone?: string | undefined;
117
+ website?: string | undefined;
118
+ enableSmsNotifications?: boolean | undefined;
119
+ }, {
120
+ email?: string | undefined;
121
+ phone?: string | undefined;
122
+ website?: string | undefined;
123
+ enableSmsNotifications?: boolean | undefined;
124
+ }>;
125
+ /**
126
+ * Education and training form schema
127
+ */
128
+ export declare const EducationAndTrainingFormSchema: z.ZodObject<{
129
+ type: z.ZodObject<{
130
+ label: z.ZodString;
131
+ value: z.ZodString;
132
+ }, "strip", z.ZodTypeAny, {
133
+ value: string;
134
+ label: string;
135
+ }, {
136
+ value: string;
137
+ label: string;
138
+ }>;
139
+ school: z.ZodString;
140
+ degree: z.ZodString;
141
+ startDate: z.ZodDate;
142
+ endDate: z.ZodNullable<z.ZodOptional<z.ZodDate>>;
143
+ isCurrentlyStudying: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodLiteral<false>]>>;
144
+ }, "strip", z.ZodTypeAny, {
145
+ type: {
146
+ value: string;
147
+ label: string;
148
+ };
149
+ school: string;
150
+ degree: string;
151
+ startDate: Date;
152
+ endDate?: Date | null | undefined;
153
+ isCurrentlyStudying?: boolean | undefined;
154
+ }, {
155
+ type: {
156
+ value: string;
157
+ label: string;
158
+ };
159
+ school: string;
160
+ degree: string;
161
+ startDate: Date;
162
+ endDate?: Date | null | undefined;
163
+ isCurrentlyStudying?: boolean | undefined;
164
+ }>;
165
+ /**
166
+ * Work history form schema
167
+ */
168
+ export declare const WorkHistoryFormSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
169
+ jobTitle: z.ZodString;
170
+ organisation: z.ZodString;
171
+ startDate: z.ZodEffects<z.ZodString, string, string>;
172
+ endDate: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
173
+ isCurrentlyWorking: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
174
+ }, "strip", z.ZodTypeAny, {
175
+ startDate: string;
176
+ jobTitle: string;
177
+ organisation: string;
178
+ endDate?: string | undefined;
179
+ isCurrentlyWorking?: boolean | undefined;
180
+ }, {
181
+ startDate: string;
182
+ jobTitle: string;
183
+ organisation: string;
184
+ endDate?: string | undefined;
185
+ isCurrentlyWorking?: boolean | undefined;
186
+ }>, {
187
+ startDate: string;
188
+ jobTitle: string;
189
+ organisation: string;
190
+ endDate?: string | undefined;
191
+ isCurrentlyWorking?: boolean | undefined;
192
+ }, {
193
+ startDate: string;
194
+ jobTitle: string;
195
+ organisation: string;
196
+ endDate?: string | undefined;
197
+ isCurrentlyWorking?: boolean | undefined;
198
+ }>, {
199
+ startDate: string;
200
+ jobTitle: string;
201
+ organisation: string;
202
+ endDate?: string | undefined;
203
+ isCurrentlyWorking?: boolean | undefined;
204
+ }, {
205
+ startDate: string;
206
+ jobTitle: string;
207
+ organisation: string;
208
+ endDate?: string | undefined;
209
+ isCurrentlyWorking?: boolean | undefined;
210
+ }>;
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkHistoryFormSchema = exports.EducationAndTrainingFormSchema = exports.ContactDetailsFormSchema = exports.participantLocationFormSchema = exports.registrationFormSchema = void 0;
4
+ const libphonenumber_js_1 = require("libphonenumber-js");
5
+ const zod_1 = require("zod");
6
+ const limits_1 = require("../constants/limits");
7
+ const enums_1 = require("../enums");
8
+ const validation_1 = require("./validation");
9
+ /**
10
+ * Registration form schema
11
+ */
12
+ exports.registrationFormSchema = zod_1.z
13
+ .object({
14
+ firstName: zod_1.z
15
+ .string()
16
+ .min(1, "First name is required")
17
+ .min(limits_1.NAME_MIN_LENGTH, `First name must be at least ${limits_1.NAME_MIN_LENGTH} characters long`)
18
+ .max(limits_1.NAME_MAX_LENGTH, `First name must be under ${limits_1.NAME_MAX_LENGTH} characters`),
19
+ lastName: zod_1.z
20
+ .string()
21
+ .min(1, "Last name is required")
22
+ .min(limits_1.NAME_MIN_LENGTH, `Last name must be at least ${limits_1.NAME_MIN_LENGTH} characters long`)
23
+ .max(limits_1.NAME_MAX_LENGTH, `Last name must be under ${limits_1.NAME_MAX_LENGTH} characters`),
24
+ email: validation_1.emailSchema,
25
+ username: zod_1.z
26
+ .string()
27
+ .min(1, "Username is required")
28
+ .min(limits_1.USERNAME_MIN_LENGTH, `Username must be at least ${limits_1.USERNAME_MIN_LENGTH} characters long`)
29
+ .max(limits_1.USERNAME_MAX_LENGTH, `Username must be under ${limits_1.USERNAME_MAX_LENGTH} characters`)
30
+ .regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores")
31
+ .refine((value) => !value.startsWith("_"), "Username cannot start with an underscore")
32
+ .refine((value) => !value.endsWith("_"), "Username cannot end with an underscore")
33
+ .refine((value) => !value.includes("__"), "Username cannot contain consecutive underscores"),
34
+ role: zod_1.z
35
+ .string()
36
+ .min(1, "Please select your role")
37
+ .refine((val) => Object.values(enums_1.UserRole).includes(val), {
38
+ message: "Please select a valid role",
39
+ }),
40
+ password: validation_1.passwordSchema,
41
+ confirmPassword: zod_1.z.string().min(1, "Confirm password is required"),
42
+ fingerPrint: zod_1.z.string().optional(),
43
+ })
44
+ // Password match validation
45
+ .refine((data) => data.password === data.confirmPassword, {
46
+ message: "Passwords don't match",
47
+ path: ["confirmPassword"],
48
+ });
49
+ /**
50
+ * Participant location form schema
51
+ */
52
+ exports.participantLocationFormSchema = zod_1.z.object({
53
+ address: zod_1.z
54
+ .object({
55
+ value: zod_1.z.string(),
56
+ label: zod_1.z.string(),
57
+ })
58
+ .refine((data) => data.value && data.label, {
59
+ message: "Address is required",
60
+ path: ["value"],
61
+ }),
62
+ state: zod_1.z
63
+ .object({
64
+ value: zod_1.z.string(),
65
+ label: zod_1.z.string(),
66
+ })
67
+ .refine((data) => data.value && data.label, {
68
+ message: "State is required",
69
+ path: ["value"],
70
+ }),
71
+ });
72
+ /**
73
+ * Contact details form schema
74
+ */
75
+ exports.ContactDetailsFormSchema = zod_1.z.object({
76
+ phone: zod_1.z
77
+ .string()
78
+ .refine((value) => {
79
+ if (!value)
80
+ return true; // Allow empty
81
+ // Validate it's Australian and mobile
82
+ if (!(0, libphonenumber_js_1.isValidPhoneNumber)(value, "AU"))
83
+ return false;
84
+ const parsed = (0, libphonenumber_js_1.parsePhoneNumberFromString)(value, "AU");
85
+ if (!parsed || parsed.country !== "AU")
86
+ return false;
87
+ const type = parsed.getType();
88
+ return type === "MOBILE" || type === "FIXED_LINE_OR_MOBILE";
89
+ }, {
90
+ message: "Please enter a valid Australian mobile number (04XX XXX XXX)",
91
+ })
92
+ .optional()
93
+ .or(zod_1.z.literal("")),
94
+ email: zod_1.z
95
+ .string()
96
+ .email("Invalid email address")
97
+ .optional()
98
+ .or(zod_1.z.literal("")),
99
+ website: zod_1.z
100
+ .string()
101
+ .regex(/^(?:https?:\/\/)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?$/, "Invalid URL")
102
+ .optional()
103
+ .or(zod_1.z.literal("")),
104
+ enableSmsNotifications: zod_1.z.boolean().optional(),
105
+ });
106
+ /**
107
+ * Education and training form schema
108
+ */
109
+ exports.EducationAndTrainingFormSchema = zod_1.z.object({
110
+ type: zod_1.z.object({
111
+ label: zod_1.z.string(),
112
+ value: zod_1.z.string(),
113
+ }),
114
+ school: zod_1.z
115
+ .string()
116
+ .min(1, "School/Instituation is required")
117
+ .max(limits_1.EDUTRAINING_INSTITUTION_MAX_LENGTH, "Institution name is too long"),
118
+ degree: zod_1.z
119
+ .string()
120
+ .min(1, "Degree is required")
121
+ .max(limits_1.EDUTRAINING_DEGREE_MAX_LENGTH, "Degree is too long"),
122
+ startDate: zod_1.z.date({
123
+ required_error: "Start date is required",
124
+ }),
125
+ endDate: zod_1.z.date().optional().nullable(),
126
+ isCurrentlyStudying: zod_1.z.boolean().or(zod_1.z.literal(false)).optional(),
127
+ });
128
+ // Helper function to check if date is in the future
129
+ const isDateInFuture = (date) => {
130
+ const today = new Date();
131
+ today.setHours(0, 0, 0, 0);
132
+ const compareDate = new Date(date);
133
+ return compareDate > today;
134
+ };
135
+ // Helper function to check if there's at least 1 month difference
136
+ const isAtLeastOneMonthDifference = (startDate, endDate) => {
137
+ const start = new Date(startDate);
138
+ const end = new Date(endDate);
139
+ // Calculate difference in months
140
+ const diffInMonths = (end.getFullYear() - start.getFullYear()) * 12 +
141
+ (end.getMonth() - start.getMonth());
142
+ return diffInMonths >= 1;
143
+ };
144
+ /**
145
+ * Work history form schema
146
+ */
147
+ exports.WorkHistoryFormSchema = zod_1.z
148
+ .object({
149
+ jobTitle: zod_1.z
150
+ .string()
151
+ .min(1, "Job title is required")
152
+ .max(limits_1.WORK_HISTORY_TITLE_MAX_LENGTH, "Job title is too long"),
153
+ organisation: zod_1.z
154
+ .string()
155
+ .min(1, "Organization is required")
156
+ .max(limits_1.WORK_HISTORY_ORGANIZATION_MAX_LENGTH, "Organization is too long"),
157
+ startDate: zod_1.z
158
+ .string()
159
+ .min(1, "Start date is required")
160
+ .refine((date) => !isDateInFuture(date), {
161
+ message: "Start date cannot be in the future",
162
+ }),
163
+ endDate: zod_1.z
164
+ .string()
165
+ .optional()
166
+ .refine((date) => !date || !isDateInFuture(date), {
167
+ message: "End date cannot be in the future",
168
+ }),
169
+ isCurrentlyWorking: zod_1.z.boolean().default(false).optional(),
170
+ })
171
+ .refine((data) => {
172
+ // Skip validation if currently working or no end date
173
+ if (data.isCurrentlyWorking || !data.endDate)
174
+ return true;
175
+ // Check if end date is after start date
176
+ const startDate = new Date(data.startDate);
177
+ const endDate = new Date(data.endDate);
178
+ return endDate > startDate;
179
+ }, {
180
+ message: "End date must be after start date",
181
+ path: ["endDate"],
182
+ })
183
+ .refine((data) => {
184
+ // Skip validation if currently working or no end date
185
+ if (data.isCurrentlyWorking || !data.endDate)
186
+ return true;
187
+ // Check if there's at least 1 month difference
188
+ return isAtLeastOneMonthDifference(data.startDate, data.endDate);
189
+ }, {
190
+ message: "There must be at least 1 month between start and end date",
191
+ path: ["endDate"],
192
+ });
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Reusable email validation schema
4
+ */
5
+ export declare const emailSchema: z.ZodString;
6
+ /**
7
+ * Reusable password validation schema
8
+ */
9
+ export declare const passwordSchema: z.ZodString;
10
+ /**
11
+ * Reusable password change schema
12
+ */
13
+ export declare const passwordChangeSchema: z.ZodEffects<z.ZodObject<{
14
+ currentPassword: z.ZodString;
15
+ newPassword: z.ZodString;
16
+ confirmPassword: z.ZodString;
17
+ }, "strip", z.ZodTypeAny, {
18
+ currentPassword: string;
19
+ newPassword: string;
20
+ confirmPassword: string;
21
+ }, {
22
+ currentPassword: string;
23
+ newPassword: string;
24
+ confirmPassword: string;
25
+ }>, {
26
+ currentPassword: string;
27
+ newPassword: string;
28
+ confirmPassword: string;
29
+ }, {
30
+ currentPassword: string;
31
+ newPassword: string;
32
+ confirmPassword: string;
33
+ }>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.passwordChangeSchema = exports.passwordSchema = exports.emailSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const limits_1 = require("../constants/limits");
6
+ /**
7
+ * Reusable email validation schema
8
+ */
9
+ exports.emailSchema = zod_1.z
10
+ .string()
11
+ .min(1, "Email is required")
12
+ .max(limits_1.EMAIL_MAX_LENGTH, "Email is too long")
13
+ .email("Invalid email")
14
+ .regex(/^[^+]+@/, "Email aliasing is not allowed");
15
+ /**
16
+ * Reusable password validation schema
17
+ */
18
+ exports.passwordSchema = zod_1.z
19
+ .string()
20
+ .min(1, "Password is required")
21
+ .min(limits_1.PASSWORD_MIN_LENGTH, `Password must be at least ${limits_1.PASSWORD_MIN_LENGTH} characters long`)
22
+ .max(limits_1.PASSWORD_MAX_LENGTH, `Password must be at most ${limits_1.PASSWORD_MAX_LENGTH} characters long`);
23
+ /**
24
+ * Reusable password change schema
25
+ */
26
+ exports.passwordChangeSchema = zod_1.z
27
+ .object({
28
+ currentPassword: zod_1.z.string().min(1, "Current password is required"),
29
+ newPassword: exports.passwordSchema,
30
+ confirmPassword: zod_1.z
31
+ .string()
32
+ .min(1, "Confirm password is required")
33
+ .max(limits_1.PASSWORD_MAX_LENGTH, "Confirm password is too long"),
34
+ })
35
+ .refine((data) => data.newPassword === data.confirmPassword, {
36
+ message: "Passwords don't match",
37
+ path: ["confirmPassword"],
38
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Format a date to display relative time (e.g., "2 hours ago", "Yesterday")
3
+ */
4
+ export declare const formatPostDate: (createdAt: Date) => string;
5
+ /**
6
+ * Format time difference between two dates (e.g., "2h ago", "3d ago")
7
+ */
8
+ export declare const formatTimeDifference: (startDate: Date, endDate?: Date) => string;
9
+ /**
10
+ * Formats date like WhatsApp:
11
+ * - For today: shows time (e.g., "2:30 PM")
12
+ * - For yesterday: shows "Yesterday"
13
+ * - For this week: shows day name (e.g., "Monday", "Tuesday")
14
+ * - For same year: shows day and month (e.g., "24 Jan")
15
+ * - For different year: shows day, month, and year (e.g., "24 Jan 2022")
16
+ */
17
+ export declare const formatWhatsAppStyle: (date: Date | string) => string;
18
+ /**
19
+ * Format job posting date consistently across the application
20
+ * Handles timezone conversion from UTC to user's local timezone
21
+ */
22
+ export declare const formatJobPostDate: (createdAt: string | Date) => string;
23
+ /**
24
+ * Format job posting date for absolute display (e.g., "Dec 15")
25
+ * Handles timezone conversion from UTC to user's local timezone
26
+ */
27
+ export declare const formatJobPostDateAbsolute: (createdAt: string | Date) => string;
28
+ /**
29
+ * Format a date/time to display in a user-friendly format
30
+ */
31
+ export declare const formatDateTime: (dateTime: string | Date) => string;
32
+ /**
33
+ * Format a date to display in a user-friendly format
34
+ */
35
+ export declare const formatDate: (dateTime: string | Date) => string;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatDate = exports.formatDateTime = exports.formatJobPostDateAbsolute = exports.formatJobPostDate = exports.formatWhatsAppStyle = exports.formatTimeDifference = exports.formatPostDate = void 0;
4
+ const date_fns_1 = require("date-fns");
5
+ /**
6
+ * Format a date to display relative time (e.g., "2 hours ago", "Yesterday")
7
+ */
8
+ const formatPostDate = (createdAt) => {
9
+ const now = new Date();
10
+ const minutesAgo = (0, date_fns_1.differenceInMinutes)(now, createdAt);
11
+ const hoursAgo = (0, date_fns_1.differenceInHours)(now, createdAt);
12
+ const daysAgo = (0, date_fns_1.differenceInDays)(now, createdAt);
13
+ if (minutesAgo < 1) {
14
+ return "Just now";
15
+ }
16
+ if (minutesAgo < 60) {
17
+ return `${minutesAgo} minute${minutesAgo > 1 ? "s" : ""} ago`;
18
+ }
19
+ if (hoursAgo < 24) {
20
+ return `${hoursAgo} hour${hoursAgo > 1 ? "s" : ""} ago`;
21
+ }
22
+ if (daysAgo === 1) {
23
+ return "Yesterday";
24
+ }
25
+ if (daysAgo <= 7) {
26
+ return `${daysAgo} day${daysAgo > 1 ? "s" : ""} ago`;
27
+ }
28
+ // For older posts, show a formatted date
29
+ return (0, date_fns_1.format)(createdAt, "MMMM d, yyyy h:mm a"); // For example: "January 15, 2023 2:45 PM"
30
+ };
31
+ exports.formatPostDate = formatPostDate;
32
+ /**
33
+ * Format time difference between two dates (e.g., "2h ago", "3d ago")
34
+ */
35
+ const formatTimeDifference = (startDate, endDate = new Date()) => {
36
+ const duration = (0, date_fns_1.intervalToDuration)({ start: startDate, end: endDate });
37
+ const minutesDifference = (0, date_fns_1.differenceInMinutes)(endDate, startDate);
38
+ // Return "just now" if less than 2 minutes
39
+ if (minutesDifference < 2) {
40
+ return "just now";
41
+ }
42
+ if (duration.years && duration.years >= 1) {
43
+ return `${duration.years}y ago`;
44
+ }
45
+ if (duration.weeks && duration.weeks >= 1) {
46
+ return `${duration.weeks}w ago`;
47
+ }
48
+ if (duration.days && duration.days >= 1) {
49
+ return `${duration.days}d ago`;
50
+ }
51
+ if (duration.hours && duration.hours >= 1) {
52
+ return `${duration.hours}h ago`;
53
+ }
54
+ if (duration.minutes && duration.minutes >= 1) {
55
+ return `${duration.minutes}m ago`;
56
+ }
57
+ return "just now"; // fallback if less than 1 minute
58
+ };
59
+ exports.formatTimeDifference = formatTimeDifference;
60
+ /**
61
+ * Formats date like WhatsApp:
62
+ * - For today: shows time (e.g., "2:30 PM")
63
+ * - For yesterday: shows "Yesterday"
64
+ * - For this week: shows day name (e.g., "Monday", "Tuesday")
65
+ * - For same year: shows day and month (e.g., "24 Jan")
66
+ * - For different year: shows day, month, and year (e.g., "24 Jan 2022")
67
+ */
68
+ const formatWhatsAppStyle = (date) => {
69
+ if (!date)
70
+ return "";
71
+ const messageDate = typeof date === "string" ? new Date(date) : date;
72
+ const now = new Date();
73
+ // Invalid date
74
+ if (isNaN(messageDate.getTime()))
75
+ return "";
76
+ // Today: show time (e.g., "2:30 PM")
77
+ if ((0, date_fns_1.isToday)(messageDate)) {
78
+ return (0, date_fns_1.format)(messageDate, "h:mm a");
79
+ }
80
+ // Yesterday: show "Yesterday"
81
+ if ((0, date_fns_1.isYesterday)(messageDate)) {
82
+ return "Yesterday";
83
+ }
84
+ // This week (but not today or yesterday): show day name (e.g., "Monday")
85
+ if ((0, date_fns_1.isSameWeek)(messageDate, now)) {
86
+ return (0, date_fns_1.format)(messageDate, "EEEE"); // Full day name
87
+ }
88
+ // Same year: show date without year (e.g., "24 Jan")
89
+ if ((0, date_fns_1.isSameYear)(messageDate, now)) {
90
+ return (0, date_fns_1.format)(messageDate, "d MMM");
91
+ }
92
+ // Different year: show date with year (e.g., "24 Jan 2022")
93
+ return (0, date_fns_1.format)(messageDate, "d MMM yyyy");
94
+ };
95
+ exports.formatWhatsAppStyle = formatWhatsAppStyle;
96
+ /**
97
+ * Format job posting date consistently across the application
98
+ * Handles timezone conversion from UTC to user's local timezone
99
+ */
100
+ const formatJobPostDate = (createdAt) => {
101
+ if (!createdAt)
102
+ return "";
103
+ const postDate = typeof createdAt === "string" ? new Date(createdAt) : createdAt;
104
+ const now = new Date();
105
+ // Invalid date
106
+ if (isNaN(postDate.getTime()))
107
+ return "";
108
+ const minutesAgo = (0, date_fns_1.differenceInMinutes)(now, postDate);
109
+ const hoursAgo = (0, date_fns_1.differenceInHours)(now, postDate);
110
+ const daysAgo = (0, date_fns_1.differenceInDays)(now, postDate);
111
+ if (minutesAgo < 1) {
112
+ return "Just now";
113
+ }
114
+ if (minutesAgo < 60) {
115
+ return `${minutesAgo} minute${minutesAgo > 1 ? "s" : ""} ago`;
116
+ }
117
+ if (hoursAgo < 24) {
118
+ return `${hoursAgo} hour${hoursAgo > 1 ? "s" : ""} ago`;
119
+ }
120
+ if (daysAgo === 1) {
121
+ return "Yesterday";
122
+ }
123
+ if (daysAgo <= 7) {
124
+ return `${daysAgo} day${daysAgo > 1 ? "s" : ""} ago`;
125
+ }
126
+ // For older posts, show formatted date in user's timezone
127
+ return (0, date_fns_1.format)(postDate, "MMM d");
128
+ };
129
+ exports.formatJobPostDate = formatJobPostDate;
130
+ /**
131
+ * Format job posting date for absolute display (e.g., "Dec 15")
132
+ * Handles timezone conversion from UTC to user's local timezone
133
+ */
134
+ const formatJobPostDateAbsolute = (createdAt) => {
135
+ if (!createdAt)
136
+ return "";
137
+ const postDate = typeof createdAt === "string" ? new Date(createdAt) : createdAt;
138
+ // Invalid date
139
+ if (isNaN(postDate.getTime()))
140
+ return "";
141
+ // Format date in user's local timezone
142
+ return (0, date_fns_1.format)(postDate, "MMM d");
143
+ };
144
+ exports.formatJobPostDateAbsolute = formatJobPostDateAbsolute;
145
+ /**
146
+ * Format a date/time to display in a user-friendly format
147
+ */
148
+ const formatDateTime = (dateTime) => {
149
+ return new Date(dateTime).toLocaleTimeString("en-US", {
150
+ hour: "numeric",
151
+ minute: "2-digit",
152
+ hour12: true,
153
+ });
154
+ };
155
+ exports.formatDateTime = formatDateTime;
156
+ /**
157
+ * Format a date to display in a user-friendly format
158
+ */
159
+ const formatDate = (dateTime) => {
160
+ return new Date(dateTime).toLocaleDateString("en-US", {
161
+ weekday: "long",
162
+ month: "short",
163
+ day: "numeric",
164
+ });
165
+ };
166
+ exports.formatDate = formatDate;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Creates a debounced version of a function
3
+ * Platform-agnostic implementation that works in both browser and Node.js
4
+ * @param func - The function to debounce
5
+ * @param delay - The delay in milliseconds
6
+ * @returns A debounced function
7
+ */
8
+ export declare function debounce<T extends (...args: any[]) => void>(func: T, delay: number): (...args: Parameters<T>) => void;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.debounce = debounce;
4
+ /**
5
+ * Creates a debounced version of a function
6
+ * Platform-agnostic implementation that works in both browser and Node.js
7
+ * @param func - The function to debounce
8
+ * @param delay - The delay in milliseconds
9
+ * @returns A debounced function
10
+ */
11
+ function debounce(func, delay) {
12
+ let timer;
13
+ return (...args) => {
14
+ if (timer !== undefined) {
15
+ clearTimeout(timer);
16
+ }
17
+ timer = setTimeout(() => {
18
+ func(...args);
19
+ timer = undefined;
20
+ }, delay);
21
+ };
22
+ }
@@ -0,0 +1,6 @@
1
+ type Option = {
2
+ label: string;
3
+ value: string;
4
+ };
5
+ export declare function enumToOptions<T extends Record<string, string>>(enumObj: T): Option[];
6
+ export {};
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enumToOptions = enumToOptions;
4
+ function enumToOptions(enumObj) {
5
+ return Object.values(enumObj).map((value) => ({
6
+ label: value, // Use the enum value for the label
7
+ value: value, // Use the enum value for the value
8
+ }));
9
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./phone";
2
+ export * from "./date";
3
+ export * from "./time";
4
+ export * from "./debounce";
5
+ export * from "./enum";
6
+ export * from "./video";
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ // Phone validation utilities
18
+ __exportStar(require("./phone"), exports);
19
+ // Date formatting utilities
20
+ __exportStar(require("./date"), exports);
21
+ // Time/shift utilities
22
+ __exportStar(require("./time"), exports);
23
+ // Functional utilities
24
+ __exportStar(require("./debounce"), exports);
25
+ // Enum utilities
26
+ __exportStar(require("./enum"), exports);
27
+ // Video validation utilities
28
+ __exportStar(require("./video"), exports);
@@ -0,0 +1,9 @@
1
+ export type PhoneValidationResult = {
2
+ isValid: boolean;
3
+ error?: string;
4
+ formattedNumber?: string;
5
+ };
6
+ /**
7
+ * Validates an Australian phone number (mobile only)
8
+ */
9
+ export declare function validateAustralianPhoneNumber(phoneNumber: string): PhoneValidationResult;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateAustralianPhoneNumber = validateAustralianPhoneNumber;
4
+ const libphonenumber_js_1 = require("libphonenumber-js");
5
+ /**
6
+ * Validates an Australian phone number (mobile only)
7
+ */
8
+ function validateAustralianPhoneNumber(phoneNumber) {
9
+ try {
10
+ // Trim whitespace
11
+ const trimmedNumber = phoneNumber.trim();
12
+ // Check if empty
13
+ if (!trimmedNumber) {
14
+ return {
15
+ isValid: false,
16
+ error: "Phone number cannot be empty",
17
+ };
18
+ }
19
+ // Check if it's a valid Australian mobile number
20
+ if (!(0, libphonenumber_js_1.isValidPhoneNumber)(trimmedNumber, "AU")) {
21
+ return {
22
+ isValid: false,
23
+ error: "Please enter a valid Australian phone number",
24
+ };
25
+ }
26
+ const parsedNumber = (0, libphonenumber_js_1.parsePhoneNumberFromString)(trimmedNumber, "AU");
27
+ if (!parsedNumber || parsedNumber.country !== "AU") {
28
+ return {
29
+ isValid: false,
30
+ error: "Please enter a valid Australian phone number",
31
+ };
32
+ }
33
+ // Check if it's a mobile number (landlines not allowed)
34
+ const numberType = parsedNumber.getType();
35
+ if (numberType !== "MOBILE" && numberType !== "FIXED_LINE_OR_MOBILE") {
36
+ return {
37
+ isValid: false,
38
+ error: "Only Australian mobile numbers are allowed",
39
+ };
40
+ }
41
+ // Just return valid - no formatting needed
42
+ return {
43
+ isValid: true,
44
+ };
45
+ }
46
+ catch {
47
+ return {
48
+ isValid: false,
49
+ error: "Please enter a valid Australian phone number",
50
+ };
51
+ }
52
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Parse time string to minutes for comparison
3
+ */
4
+ export declare const parseTimeToMinutes: (timeStr: string) => number;
5
+ /**
6
+ * Get shift duration in minutes
7
+ */
8
+ export declare const getShiftDurationMinutes: (startTime: string, endTime: string) => number;
9
+ /**
10
+ * Get shift duration in hours
11
+ */
12
+ export declare const getShiftDurationHours: (startTime: string, endTime: string) => number;
13
+ /**
14
+ * Check if two shifts overlap
15
+ */
16
+ export declare const areShiftsOverlapping: (shift1: {
17
+ startTime: string;
18
+ endTime: string;
19
+ }, shift2: {
20
+ startTime: string;
21
+ endTime: string;
22
+ }) => boolean;
23
+ /**
24
+ * Check if shifts overlap for a given day
25
+ */
26
+ export declare const checkForOverlappingShifts: (shifts: {
27
+ startTime: string;
28
+ endTime: string;
29
+ }[]) => boolean;
30
+ /**
31
+ * Check if a shift is too short (less than minimum minutes)
32
+ */
33
+ export declare const isShiftTooShort: (shift: {
34
+ startTime: string;
35
+ endTime: string;
36
+ }, minimumMinutes?: number) => boolean;
37
+ /**
38
+ * Validate a single shift
39
+ */
40
+ export declare const validateShift: (shift: {
41
+ startTime: string;
42
+ endTime: string;
43
+ }, allShifts: {
44
+ startTime: string;
45
+ endTime: string;
46
+ }[], shiftIndex: number) => {
47
+ errors: string[];
48
+ warnings: string[];
49
+ };
50
+ /**
51
+ * Generate a new shift time based on the last shift
52
+ */
53
+ export declare const generateNewShiftTime: (lastShift?: {
54
+ startTime: string;
55
+ endTime: string;
56
+ }) => {
57
+ startTime: string;
58
+ endTime: string;
59
+ };
60
+ /**
61
+ * Convert time string to ISO format
62
+ */
63
+ export declare const timeStringToISO: (timeString: string) => string;
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.timeStringToISO = exports.generateNewShiftTime = exports.validateShift = exports.isShiftTooShort = exports.checkForOverlappingShifts = exports.areShiftsOverlapping = exports.getShiftDurationHours = exports.getShiftDurationMinutes = exports.parseTimeToMinutes = void 0;
4
+ const date_fns_1 = require("date-fns");
5
+ /**
6
+ * Parse time string to minutes for comparison
7
+ */
8
+ const parseTimeToMinutes = (timeStr) => {
9
+ const [hours, minutes] = timeStr.split(":").map(Number);
10
+ return hours * 60 + minutes;
11
+ };
12
+ exports.parseTimeToMinutes = parseTimeToMinutes;
13
+ /**
14
+ * Get shift duration in minutes
15
+ */
16
+ const getShiftDurationMinutes = (startTime, endTime) => {
17
+ return (0, exports.parseTimeToMinutes)(endTime) - (0, exports.parseTimeToMinutes)(startTime);
18
+ };
19
+ exports.getShiftDurationMinutes = getShiftDurationMinutes;
20
+ /**
21
+ * Get shift duration in hours
22
+ */
23
+ const getShiftDurationHours = (startTime, endTime) => {
24
+ return (0, exports.getShiftDurationMinutes)(startTime, endTime) / 60;
25
+ };
26
+ exports.getShiftDurationHours = getShiftDurationHours;
27
+ /**
28
+ * Check if two shifts overlap
29
+ */
30
+ const areShiftsOverlapping = (shift1, shift2) => {
31
+ const start1 = (0, exports.parseTimeToMinutes)(shift1.startTime);
32
+ const end1 = (0, exports.parseTimeToMinutes)(shift1.endTime);
33
+ const start2 = (0, exports.parseTimeToMinutes)(shift2.startTime);
34
+ const end2 = (0, exports.parseTimeToMinutes)(shift2.endTime);
35
+ return start1 < end2 && start2 < end1;
36
+ };
37
+ exports.areShiftsOverlapping = areShiftsOverlapping;
38
+ /**
39
+ * Check if shifts overlap for a given day
40
+ */
41
+ const checkForOverlappingShifts = (shifts) => {
42
+ if (shifts.length <= 1)
43
+ return false;
44
+ // Sort shifts by start time
45
+ const sortedShifts = [...shifts].sort((a, b) => {
46
+ return (new Date(`2023-01-01T${a.startTime}`).getTime() -
47
+ new Date(`2023-01-01T${b.startTime}`).getTime());
48
+ });
49
+ // Check for overlaps
50
+ for (let i = 0; i < sortedShifts.length - 1; i++) {
51
+ const currentShiftEnd = new Date(`2023-01-01T${sortedShifts[i].endTime}`);
52
+ const nextShiftStart = new Date(`2023-01-01T${sortedShifts[i + 1].startTime}`);
53
+ if (currentShiftEnd > nextShiftStart) {
54
+ return true; // Found an overlap
55
+ }
56
+ }
57
+ return false;
58
+ };
59
+ exports.checkForOverlappingShifts = checkForOverlappingShifts;
60
+ /**
61
+ * Check if a shift is too short (less than minimum minutes)
62
+ */
63
+ const isShiftTooShort = (shift, minimumMinutes = 60) => {
64
+ const startTime = new Date(`2023-01-01T${shift.startTime}`);
65
+ const endTime = new Date(`2023-01-01T${shift.endTime}`);
66
+ // Calculate difference in minutes
67
+ const diffMinutes = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
68
+ return diffMinutes < minimumMinutes;
69
+ };
70
+ exports.isShiftTooShort = isShiftTooShort;
71
+ /**
72
+ * Validate a single shift
73
+ */
74
+ const validateShift = (shift, allShifts, shiftIndex) => {
75
+ const errors = [];
76
+ const warnings = [];
77
+ // 1. Check if times are provided
78
+ if (!shift.startTime || !shift.endTime) {
79
+ errors.push("Both start and end times are required");
80
+ return { errors, warnings };
81
+ }
82
+ // 2. End time after start time
83
+ if ((0, exports.parseTimeToMinutes)(shift.endTime) <= (0, exports.parseTimeToMinutes)(shift.startTime)) {
84
+ errors.push("End time must be after start time");
85
+ }
86
+ else {
87
+ // 3. Minimum shift duration (1 hour)
88
+ const durationHours = (0, exports.getShiftDurationHours)(shift.startTime, shift.endTime);
89
+ if (durationHours < 1) {
90
+ errors.push("Shift must be at least 1 hour");
91
+ }
92
+ // 4. Maximum shift duration (12 hours)
93
+ if (durationHours > 12) {
94
+ errors.push("Shift must be at most 12 hours");
95
+ }
96
+ }
97
+ // 5. Check for overlapping shifts
98
+ const otherShifts = [...allShifts];
99
+ otherShifts.splice(shiftIndex, 1); // Remove current shift from array
100
+ for (let i = 0; i < otherShifts.length; i++) {
101
+ if ((0, exports.areShiftsOverlapping)(shift, otherShifts[i])) {
102
+ errors.push("Shifts cannot overlap");
103
+ break;
104
+ }
105
+ }
106
+ return { errors, warnings };
107
+ };
108
+ exports.validateShift = validateShift;
109
+ /**
110
+ * Generate a new shift time based on the last shift
111
+ */
112
+ const generateNewShiftTime = (lastShift) => {
113
+ let newStartTime = "09:00";
114
+ let newEndTime = "10:00";
115
+ if (lastShift) {
116
+ // Start the new shift right after the last shift ends (no gap)
117
+ const lastEndTime = new Date(`2023-01-01T${lastShift.endTime}`);
118
+ // Format the new start time
119
+ newStartTime = (0, date_fns_1.format)(lastEndTime, "HH:mm");
120
+ // Set end time 1 hour after start time
121
+ const newEnd = new Date(lastEndTime);
122
+ newEnd.setHours(newEnd.getHours() + 1);
123
+ newEndTime = (0, date_fns_1.format)(newEnd, "HH:mm");
124
+ }
125
+ return { startTime: newStartTime, endTime: newEndTime };
126
+ };
127
+ exports.generateNewShiftTime = generateNewShiftTime;
128
+ /**
129
+ * Convert time string to ISO format
130
+ */
131
+ const timeStringToISO = (timeString) => {
132
+ return new Date(`2023-01-01T${timeString}`).toISOString();
133
+ };
134
+ exports.timeStringToISO = timeStringToISO;
@@ -0,0 +1,25 @@
1
+ export type VideoProvider = "youtube" | "vimeo" | "loom" | "tiktok";
2
+ export interface VideoValidationResult {
3
+ isValid: boolean;
4
+ provider?: VideoProvider;
5
+ error?: string;
6
+ }
7
+ /**
8
+ * Unified video link validation function
9
+ * @param url - The video URL to validate
10
+ * @param allowedProviders - Array of allowed video providers
11
+ * @returns VideoValidationResult object
12
+ */
13
+ export declare function validateVideoLink(url: string, allowedProviders: VideoProvider[]): VideoValidationResult;
14
+ /**
15
+ * Detect video provider from URL
16
+ */
17
+ export declare function detectVideoProvider(url: string): VideoProvider | null;
18
+ /**
19
+ * Check if URL is valid YouTube or Vimeo URL
20
+ */
21
+ export declare function isValidYouTubeOrVimeoUrl(url: string): boolean;
22
+ /**
23
+ * Check if URL is valid community video URL (YouTube, Vimeo, Loom, TikTok)
24
+ */
25
+ export declare function isValidCommunityVideoUrl(url: string): boolean;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateVideoLink = validateVideoLink;
4
+ exports.detectVideoProvider = detectVideoProvider;
5
+ exports.isValidYouTubeOrVimeoUrl = isValidYouTubeOrVimeoUrl;
6
+ exports.isValidCommunityVideoUrl = isValidCommunityVideoUrl;
7
+ /**
8
+ * Unified video link validation function
9
+ * @param url - The video URL to validate
10
+ * @param allowedProviders - Array of allowed video providers
11
+ * @returns VideoValidationResult object
12
+ */
13
+ function validateVideoLink(url, allowedProviders) {
14
+ if (!url || url.trim() === "") {
15
+ return { isValid: true }; // Empty URL is valid (optional field)
16
+ }
17
+ const trimmedUrl = url.trim();
18
+ // Validate URL format
19
+ if (!isValidURL(trimmedUrl)) {
20
+ return { isValid: false, error: "Invalid URL format" };
21
+ }
22
+ const provider = detectVideoProvider(trimmedUrl);
23
+ if (!provider) {
24
+ return {
25
+ isValid: false,
26
+ error: `Unsupported video provider. Supported: ${allowedProviders.join(", ")}`,
27
+ };
28
+ }
29
+ if (!allowedProviders.includes(provider)) {
30
+ return {
31
+ isValid: false,
32
+ error: `${provider} is not allowed. Supported: ${allowedProviders.join(", ")}`,
33
+ };
34
+ }
35
+ return { isValid: true, provider };
36
+ }
37
+ /**
38
+ * Detect video provider from URL
39
+ */
40
+ function detectVideoProvider(url) {
41
+ const trimmedUrl = url.trim().toLowerCase();
42
+ if (trimmedUrl.includes("youtube.com") || trimmedUrl.includes("youtu.be")) {
43
+ return "youtube";
44
+ }
45
+ if (trimmedUrl.includes("vimeo.com")) {
46
+ return "vimeo";
47
+ }
48
+ if (trimmedUrl.includes("loom.com")) {
49
+ return "loom";
50
+ }
51
+ if (trimmedUrl.includes("tiktok.com")) {
52
+ return "tiktok";
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Check if URL is valid YouTube or Vimeo URL
58
+ */
59
+ function isValidYouTubeOrVimeoUrl(url) {
60
+ const provider = detectVideoProvider(url);
61
+ return provider === "youtube" || provider === "vimeo";
62
+ }
63
+ /**
64
+ * Check if URL is valid community video URL (YouTube, Vimeo, Loom, TikTok)
65
+ */
66
+ function isValidCommunityVideoUrl(url) {
67
+ return detectVideoProvider(url) !== null;
68
+ }
69
+ /**
70
+ * Basic URL validation
71
+ */
72
+ function isValidURL(url) {
73
+ try {
74
+ new URL(url);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@careflair/common",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Shared assets for CareFlair",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,6 +17,8 @@
17
17
  "author": "Farhan Hossain",
18
18
  "license": "ISC",
19
19
  "dependencies": {
20
+ "date-fns": "^4.1.0",
21
+ "libphonenumber-js": "^1.12.25",
20
22
  "zod": "^3.23.8"
21
23
  },
22
24
  "files": [