@tuturuuu/utils 0.0.2 → 0.6.0
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/CHANGELOG.md +305 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +122 -3
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- package/src/index.ts +0 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
4
|
+
import utc from 'dayjs/plugin/utc';
|
|
5
|
+
|
|
6
|
+
dayjs.extend(timezone);
|
|
7
|
+
dayjs.extend(utc);
|
|
8
|
+
|
|
9
|
+
export function isAllDayEvent(
|
|
10
|
+
event: Pick<CalendarEvent, 'start_at' | 'end_at'>
|
|
11
|
+
): boolean {
|
|
12
|
+
const start = dayjs(event.start_at);
|
|
13
|
+
const end = dayjs(event.end_at);
|
|
14
|
+
|
|
15
|
+
const durationMs = end.diff(start, 'millisecond');
|
|
16
|
+
const isMultipleOf24Hours = durationMs % (24 * 60 * 60 * 1000) === 0;
|
|
17
|
+
|
|
18
|
+
return durationMs > 0 && isMultipleOf24Hours;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Helper function to convert Google Calendar all-day events to proper timezone
|
|
22
|
+
export function convertGoogleAllDayEvent(
|
|
23
|
+
startDate: string | undefined,
|
|
24
|
+
endDate: string | undefined,
|
|
25
|
+
userTimezone?: string
|
|
26
|
+
): { start_at: string; end_at: string } {
|
|
27
|
+
// Check if this is a date-only format (all-day event from Google)
|
|
28
|
+
const isDateOnly = (dateStr: string) => /^\d{4}-\d{2}-\d{2}$/.test(dateStr);
|
|
29
|
+
|
|
30
|
+
if (!startDate || !endDate) {
|
|
31
|
+
const now = dayjs();
|
|
32
|
+
return {
|
|
33
|
+
start_at: now.toISOString(),
|
|
34
|
+
end_at: now.add(1, 'hour').toISOString(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If both are date-only (Google all-day event), convert to user's timezone midnight
|
|
39
|
+
if (isDateOnly(startDate) && isDateOnly(endDate)) {
|
|
40
|
+
const tz =
|
|
41
|
+
userTimezone === 'auto'
|
|
42
|
+
? typeof window !== 'undefined'
|
|
43
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
44
|
+
: undefined
|
|
45
|
+
: userTimezone;
|
|
46
|
+
|
|
47
|
+
const startAtMidnight = tz
|
|
48
|
+
? dayjs.tz(`${startDate}T00:00:00`, tz)
|
|
49
|
+
: dayjs(`${startDate}T00:00:00`);
|
|
50
|
+
|
|
51
|
+
const endAtMidnight = tz
|
|
52
|
+
? dayjs.tz(`${endDate}T00:00:00`, tz)
|
|
53
|
+
: dayjs(`${endDate}T00:00:00`);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
start_at: startAtMidnight.toISOString(),
|
|
57
|
+
end_at: endAtMidnight.toISOString(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Otherwise, use the dates as-is (they're already dateTime format)
|
|
62
|
+
return {
|
|
63
|
+
start_at: startDate,
|
|
64
|
+
end_at: endDate,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper to create an all-day event in user's timezone
|
|
69
|
+
export function createAllDayEvent(
|
|
70
|
+
date: Date,
|
|
71
|
+
userTimezone?: string,
|
|
72
|
+
durationDays: number = 1
|
|
73
|
+
): { start_at: string; end_at: string } {
|
|
74
|
+
const tz =
|
|
75
|
+
userTimezone === 'auto'
|
|
76
|
+
? typeof window !== 'undefined'
|
|
77
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
78
|
+
: undefined
|
|
79
|
+
: userTimezone;
|
|
80
|
+
|
|
81
|
+
const startAtMidnight = tz
|
|
82
|
+
? dayjs.tz(date, tz).startOf('day')
|
|
83
|
+
: dayjs(date).startOf('day');
|
|
84
|
+
|
|
85
|
+
const endAtMidnight = startAtMidnight.add(durationDays, 'day');
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
start_at: startAtMidnight.toISOString(),
|
|
89
|
+
end_at: endAtMidnight.toISOString(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export const getEventStyles = (
|
|
2
|
+
color: string
|
|
3
|
+
): {
|
|
4
|
+
bg: string;
|
|
5
|
+
border: string;
|
|
6
|
+
text: string;
|
|
7
|
+
dragBg: string;
|
|
8
|
+
syncingBg: string;
|
|
9
|
+
successBg: string;
|
|
10
|
+
errorBg: string;
|
|
11
|
+
} => {
|
|
12
|
+
const colorStyles = {
|
|
13
|
+
BLUE: {
|
|
14
|
+
bg: 'bg-calendar-bg-blue hover:ring-dynamic-light-blue/80',
|
|
15
|
+
border: 'border-dynamic-light-blue/80',
|
|
16
|
+
text: 'text-dynamic-light-blue',
|
|
17
|
+
dragBg: 'bg-calendar-bg-blue',
|
|
18
|
+
syncingBg: 'bg-calendar-bg-blue',
|
|
19
|
+
successBg: 'bg-calendar-bg-blue',
|
|
20
|
+
errorBg: 'bg-calendar-bg-red',
|
|
21
|
+
},
|
|
22
|
+
RED: {
|
|
23
|
+
bg: 'bg-calendar-bg-red hover:ring-dynamic-light-red/80',
|
|
24
|
+
border: 'border-dynamic-light-red/80',
|
|
25
|
+
text: 'text-dynamic-light-red',
|
|
26
|
+
dragBg: 'bg-calendar-bg-red',
|
|
27
|
+
syncingBg: 'bg-calendar-bg-red',
|
|
28
|
+
successBg: 'bg-calendar-bg-red',
|
|
29
|
+
errorBg: 'bg-calendar-bg-red',
|
|
30
|
+
},
|
|
31
|
+
GREEN: {
|
|
32
|
+
bg: 'bg-calendar-bg-green hover:ring-dynamic-light-green/80',
|
|
33
|
+
border: 'border-dynamic-light-green/80',
|
|
34
|
+
text: 'text-dynamic-light-green',
|
|
35
|
+
dragBg: 'bg-calendar-bg-green',
|
|
36
|
+
syncingBg: 'bg-calendar-bg-green',
|
|
37
|
+
successBg: 'bg-calendar-bg-green',
|
|
38
|
+
errorBg: 'bg-calendar-bg-red',
|
|
39
|
+
},
|
|
40
|
+
YELLOW: {
|
|
41
|
+
bg: 'bg-calendar-bg-yellow hover:ring-dynamic-light-yellow/80',
|
|
42
|
+
border: 'border-dynamic-light-yellow/80',
|
|
43
|
+
text: 'text-dynamic-light-yellow',
|
|
44
|
+
dragBg: 'bg-calendar-bg-yellow',
|
|
45
|
+
syncingBg: 'bg-calendar-bg-yellow',
|
|
46
|
+
successBg: 'bg-calendar-bg-yellow',
|
|
47
|
+
errorBg: 'bg-calendar-bg-red',
|
|
48
|
+
},
|
|
49
|
+
PURPLE: {
|
|
50
|
+
bg: 'bg-calendar-bg-purple hover:ring-dynamic-light-purple/80',
|
|
51
|
+
border: 'border-dynamic-light-purple/80',
|
|
52
|
+
text: 'text-dynamic-light-purple',
|
|
53
|
+
dragBg: 'bg-calendar-bg-purple',
|
|
54
|
+
syncingBg: 'bg-calendar-bg-purple',
|
|
55
|
+
successBg: 'bg-calendar-bg-purple',
|
|
56
|
+
errorBg: 'bg-calendar-bg-red',
|
|
57
|
+
},
|
|
58
|
+
PINK: {
|
|
59
|
+
bg: 'bg-calendar-bg-pink hover:ring-dynamic-light-pink/80',
|
|
60
|
+
border: 'border-dynamic-light-pink/80',
|
|
61
|
+
text: 'text-dynamic-light-pink',
|
|
62
|
+
dragBg: 'bg-calendar-bg-pink',
|
|
63
|
+
syncingBg: 'bg-calendar-bg-pink',
|
|
64
|
+
successBg: 'bg-calendar-bg-pink',
|
|
65
|
+
errorBg: 'bg-calendar-bg-red',
|
|
66
|
+
},
|
|
67
|
+
ORANGE: {
|
|
68
|
+
bg: 'bg-calendar-bg-orange hover:ring-dynamic-light-orange/80',
|
|
69
|
+
border: 'border-dynamic-light-orange/80',
|
|
70
|
+
text: 'text-dynamic-light-orange',
|
|
71
|
+
dragBg: 'bg-calendar-bg-orange',
|
|
72
|
+
syncingBg: 'bg-calendar-bg-orange',
|
|
73
|
+
successBg: 'bg-calendar-bg-orange',
|
|
74
|
+
errorBg: 'bg-calendar-bg-red',
|
|
75
|
+
},
|
|
76
|
+
INDIGO: {
|
|
77
|
+
bg: 'bg-calendar-bg-indigo hover:ring-dynamic-light-indigo/80',
|
|
78
|
+
border: 'border-dynamic-light-indigo/80',
|
|
79
|
+
text: 'text-dynamic-light-indigo',
|
|
80
|
+
dragBg: 'bg-calendar-bg-indigo',
|
|
81
|
+
syncingBg: 'bg-calendar-bg-indigo',
|
|
82
|
+
successBg: 'bg-calendar-bg-indigo',
|
|
83
|
+
errorBg: 'bg-calendar-bg-red',
|
|
84
|
+
},
|
|
85
|
+
CYAN: {
|
|
86
|
+
bg: 'bg-calendar-bg-cyan hover:ring-dynamic-light-cyan/80',
|
|
87
|
+
border: 'border-dynamic-light-cyan/80',
|
|
88
|
+
text: 'text-dynamic-light-cyan',
|
|
89
|
+
dragBg: 'bg-calendar-bg-cyan',
|
|
90
|
+
syncingBg: 'bg-calendar-bg-cyan',
|
|
91
|
+
successBg: 'bg-calendar-bg-cyan',
|
|
92
|
+
errorBg: 'bg-calendar-bg-red',
|
|
93
|
+
},
|
|
94
|
+
GRAY: {
|
|
95
|
+
bg: 'bg-calendar-bg-gray hover:ring-dynamic-light-gray/80',
|
|
96
|
+
border: 'border-dynamic-light-gray/80',
|
|
97
|
+
text: 'text-dynamic-light-gray',
|
|
98
|
+
dragBg: 'bg-calendar-bg-gray',
|
|
99
|
+
syncingBg: 'bg-calendar-bg-gray',
|
|
100
|
+
successBg: 'bg-calendar-bg-gray',
|
|
101
|
+
errorBg: 'bg-calendar-bg-red',
|
|
102
|
+
},
|
|
103
|
+
} as const;
|
|
104
|
+
|
|
105
|
+
const normalizedColor = color?.toUpperCase() ?? 'BLUE';
|
|
106
|
+
const colorStyle = colorStyles[normalizedColor as keyof typeof colorStyles];
|
|
107
|
+
|
|
108
|
+
if (!colorStyle) return colorStyles.BLUE;
|
|
109
|
+
return colorStyle;
|
|
110
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Metadata, Viewport } from 'next';
|
|
2
|
+
import { Noto_Sans } from 'next/font/google';
|
|
3
|
+
import { DEV_MODE } from '../constants';
|
|
4
|
+
|
|
5
|
+
export const font = Noto_Sans({
|
|
6
|
+
subsets: ['latin', 'vietnamese'],
|
|
7
|
+
display: 'block',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
interface MetadataProps {
|
|
11
|
+
config: {
|
|
12
|
+
name: string;
|
|
13
|
+
url: string;
|
|
14
|
+
ogImage: string;
|
|
15
|
+
keywords?: string[];
|
|
16
|
+
description: {
|
|
17
|
+
en: string;
|
|
18
|
+
vi?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
params: Promise<{
|
|
22
|
+
locale: string;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function generateCommonMetadata({
|
|
27
|
+
config,
|
|
28
|
+
params,
|
|
29
|
+
}: MetadataProps): Promise<Metadata> {
|
|
30
|
+
const { locale } = await params;
|
|
31
|
+
const description =
|
|
32
|
+
locale === 'vi' ? config.description.vi : config.description.en;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
applicationName: config.name,
|
|
36
|
+
title: {
|
|
37
|
+
default: config.name,
|
|
38
|
+
template: `${DEV_MODE ? '[DEV] ' : ''} %s | ${config.name}`,
|
|
39
|
+
},
|
|
40
|
+
metadataBase: new URL(config.url),
|
|
41
|
+
description,
|
|
42
|
+
keywords: config.keywords ?? [
|
|
43
|
+
'React.js',
|
|
44
|
+
'Next.js',
|
|
45
|
+
'Tailwind CSS',
|
|
46
|
+
'TypeScript',
|
|
47
|
+
'Biome',
|
|
48
|
+
],
|
|
49
|
+
appleWebApp: {
|
|
50
|
+
capable: true,
|
|
51
|
+
statusBarStyle: 'default',
|
|
52
|
+
title: config.name,
|
|
53
|
+
},
|
|
54
|
+
formatDetection: {
|
|
55
|
+
telephone: false,
|
|
56
|
+
},
|
|
57
|
+
openGraph: {
|
|
58
|
+
type: 'website',
|
|
59
|
+
locale,
|
|
60
|
+
url: config.url,
|
|
61
|
+
title: config.name,
|
|
62
|
+
description,
|
|
63
|
+
siteName: config.name,
|
|
64
|
+
images: [
|
|
65
|
+
{
|
|
66
|
+
url: config.ogImage,
|
|
67
|
+
width: 1200,
|
|
68
|
+
height: 630,
|
|
69
|
+
alt: config.name,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
twitter: {
|
|
74
|
+
card: 'summary_large_image',
|
|
75
|
+
title: config.name,
|
|
76
|
+
description,
|
|
77
|
+
images: [config.ogImage],
|
|
78
|
+
creator: '@tuturuuu',
|
|
79
|
+
},
|
|
80
|
+
icons: {
|
|
81
|
+
icon: '/favicon.ico',
|
|
82
|
+
shortcut: '/favicon-16x16.png',
|
|
83
|
+
apple: '/apple-touch-icon.png',
|
|
84
|
+
},
|
|
85
|
+
manifest: '/manifest.webmanifest',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const viewport: Viewport = {
|
|
90
|
+
width: 'device-width',
|
|
91
|
+
initialScale: 1,
|
|
92
|
+
maximumScale: 5,
|
|
93
|
+
viewportFit: 'cover',
|
|
94
|
+
themeColor: [
|
|
95
|
+
{ media: '(prefers-color-scheme: dark)', color: 'black' },
|
|
96
|
+
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
|
97
|
+
],
|
|
98
|
+
colorScheme: 'dark light',
|
|
99
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DEV_MODE } from '../constants';
|
|
2
|
+
|
|
3
|
+
const enabled = false;
|
|
4
|
+
|
|
5
|
+
export const ReactScan = () => {
|
|
6
|
+
if (!DEV_MODE || !enabled) return null;
|
|
7
|
+
return (
|
|
8
|
+
<head>
|
|
9
|
+
<script
|
|
10
|
+
crossOrigin="anonymous"
|
|
11
|
+
src="//unpkg.com/react-scan/dist/auto.global.js"
|
|
12
|
+
/>
|
|
13
|
+
</head>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { WorkspaceConfig } from '@tuturuuu/types/primitives/WorkspaceConfig';
|
|
2
|
+
|
|
3
|
+
// Report template configs
|
|
4
|
+
export const reportConfigs: (WorkspaceConfig & {
|
|
5
|
+
defaultValue: string;
|
|
6
|
+
})[] = [
|
|
7
|
+
{
|
|
8
|
+
id: 'BRAND_LOGO_URL',
|
|
9
|
+
type: 'URL',
|
|
10
|
+
defaultValue: '',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'BRAND_NAME',
|
|
14
|
+
type: 'TEXT',
|
|
15
|
+
defaultValue: '',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'BRAND_LOCATION',
|
|
19
|
+
type: 'TEXT',
|
|
20
|
+
defaultValue: '',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'BRAND_PHONE_NUMBER',
|
|
24
|
+
type: 'TEXT',
|
|
25
|
+
defaultValue: '',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'REPORT_TITLE_PREFIX',
|
|
29
|
+
type: 'TEXT',
|
|
30
|
+
defaultValue: '',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'REPORT_TITLE_SUFFIX',
|
|
34
|
+
type: 'TEXT',
|
|
35
|
+
defaultValue: '',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'REPORT_DEFAULT_TITLE',
|
|
39
|
+
type: 'TEXT',
|
|
40
|
+
defaultValue: '',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'REPORT_INTRO',
|
|
44
|
+
type: 'TEXT',
|
|
45
|
+
defaultValue: '',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'REPORT_CONTENT_TEXT',
|
|
49
|
+
type: 'TEXT',
|
|
50
|
+
defaultValue: '',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'REPORT_SCORE_TEXT',
|
|
54
|
+
type: 'TEXT',
|
|
55
|
+
defaultValue: '',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'REPORT_FEEDBACK_TEXT',
|
|
59
|
+
type: 'TEXT',
|
|
60
|
+
defaultValue: '',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'REPORT_CONCLUSION',
|
|
64
|
+
type: 'TEXT',
|
|
65
|
+
defaultValue: '',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'REPORT_CLOSING',
|
|
69
|
+
type: 'TEXT',
|
|
70
|
+
defaultValue: '',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'ENABLE_REPORT_EXPORT_ONLY_APPROVED',
|
|
74
|
+
type: 'TEXT',
|
|
75
|
+
defaultValue: 'false',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'ENABLE_REPORT_PENDING_WATERMARK',
|
|
79
|
+
type: 'TEXT',
|
|
80
|
+
defaultValue: 'false',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Lead generation email template configs
|
|
85
|
+
export const leadGenerationConfigs: (WorkspaceConfig & {
|
|
86
|
+
defaultValue: string;
|
|
87
|
+
})[] = [
|
|
88
|
+
// Shared brand configs (already in report template)
|
|
89
|
+
{
|
|
90
|
+
id: 'BRAND_LOGO_URL',
|
|
91
|
+
type: 'URL',
|
|
92
|
+
defaultValue: '',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'BRAND_NAME',
|
|
96
|
+
type: 'TEXT',
|
|
97
|
+
defaultValue: '',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'BRAND_LOCATION',
|
|
101
|
+
type: 'TEXT',
|
|
102
|
+
defaultValue: '',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'BRAND_PHONE_NUMBER',
|
|
106
|
+
type: 'TEXT',
|
|
107
|
+
defaultValue: '',
|
|
108
|
+
},
|
|
109
|
+
// Lead generation specific configs
|
|
110
|
+
{
|
|
111
|
+
id: 'LEAD_EMAIL_TITLE',
|
|
112
|
+
type: 'TEXT',
|
|
113
|
+
defaultValue: '',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'LEAD_EMAIL_GREETING',
|
|
117
|
+
type: 'TEXT',
|
|
118
|
+
defaultValue: '',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'LEAD_EMAIL_TABLE_HEADER_COMMENTS',
|
|
122
|
+
type: 'TEXT',
|
|
123
|
+
defaultValue: '',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'LEAD_EMAIL_TABLE_HEADER_SCORE',
|
|
127
|
+
type: 'TEXT',
|
|
128
|
+
defaultValue: '',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'LEAD_EMAIL_TABLE_SCORE_SCALE',
|
|
132
|
+
type: 'TEXT',
|
|
133
|
+
defaultValue: '',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'LEAD_EMAIL_FOOTER',
|
|
137
|
+
type: 'TEXT',
|
|
138
|
+
defaultValue: '',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'LEAD_EMAIL_SIGNATURE_TITLE',
|
|
142
|
+
type: 'TEXT',
|
|
143
|
+
defaultValue: '',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'LEAD_EMAIL_SIGNATURE_NAME',
|
|
147
|
+
type: 'TEXT',
|
|
148
|
+
defaultValue: '',
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
// Combined list for backward compatibility
|
|
153
|
+
export const availableConfigs: (WorkspaceConfig & {
|
|
154
|
+
defaultValue: string;
|
|
155
|
+
})[] = [
|
|
156
|
+
...reportConfigs,
|
|
157
|
+
...leadGenerationConfigs.filter(
|
|
158
|
+
(config) => !reportConfigs.some((rc) => rc.id === config.id)
|
|
159
|
+
),
|
|
160
|
+
];
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const GITHUB_OWNER = 'tutur3u';
|
|
2
|
+
export const GITHUB_REPO = 'platform';
|
|
3
|
+
export const ROOT_WORKSPACE_ID = '00000000-0000-0000-0000-000000000000';
|
|
4
|
+
export const INTERNAL_WORKSPACE_SLUG = 'internal';
|
|
5
|
+
export const PERSONAL_WORKSPACE_SLUG = 'personal';
|
|
6
|
+
|
|
7
|
+
// Workspace creation limits
|
|
8
|
+
export const MAX_WORKSPACES_FOR_FREE_USERS = 10;
|
|
9
|
+
|
|
10
|
+
export const resolveWorkspaceId = (identifier: string): string => {
|
|
11
|
+
if (!identifier) return identifier;
|
|
12
|
+
|
|
13
|
+
if (identifier.toLowerCase() === INTERNAL_WORKSPACE_SLUG) {
|
|
14
|
+
return ROOT_WORKSPACE_ID;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return identifier;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const normalizeWorkspaceContextId = (identifier?: string | null) => {
|
|
21
|
+
const trimmed = identifier?.trim();
|
|
22
|
+
if (!trimmed) return PERSONAL_WORKSPACE_SLUG;
|
|
23
|
+
|
|
24
|
+
if (trimmed.toLowerCase() === PERSONAL_WORKSPACE_SLUG) {
|
|
25
|
+
return PERSONAL_WORKSPACE_SLUG;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return resolveWorkspaceId(trimmed);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const toWorkspaceSlug = (
|
|
32
|
+
workspaceId: string,
|
|
33
|
+
{ personal = false }: { personal?: boolean } = {}
|
|
34
|
+
): string => {
|
|
35
|
+
if (personal) return PERSONAL_WORKSPACE_SLUG;
|
|
36
|
+
if (workspaceId === ROOT_WORKSPACE_ID) return INTERNAL_WORKSPACE_SLUG;
|
|
37
|
+
return workspaceId;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const isInternalWorkspaceSlug = (
|
|
41
|
+
identifier?: string | null
|
|
42
|
+
): boolean => {
|
|
43
|
+
if (!identifier) return false;
|
|
44
|
+
return identifier.toLowerCase() === INTERNAL_WORKSPACE_SLUG;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const DEV_MODE = process.env.NODE_ENV === 'development';
|
|
48
|
+
export const PROD_MODE = process.env.NODE_ENV === 'production';
|
|
49
|
+
|
|
50
|
+
// ── Generic text field length tiers ──────────────────────────────────────────
|
|
51
|
+
// Use these when a domain-specific constant below does not exist.
|
|
52
|
+
export const MAX_CODE_LENGTH = 10; // locale codes, currency codes
|
|
53
|
+
export const MAX_OTP_LENGTH = 20; // OTP / verification codes
|
|
54
|
+
export const MAX_IP_LENGTH = 45; // IPv6 addresses
|
|
55
|
+
export const MAX_COLOR_LENGTH = 50; // CSS color values, hex strings
|
|
56
|
+
export const MAX_DATE_STRING_LENGTH = 50; // ISO date strings
|
|
57
|
+
export const MAX_SHORT_TEXT_LENGTH = 100; // status, type, timezone, provider
|
|
58
|
+
export const MAX_ID_LENGTH = 255; // non-UUID identifiers, slugs
|
|
59
|
+
export const MAX_NAME_LENGTH = 255; // generic names, titles, labels
|
|
60
|
+
export const MAX_SEARCH_LENGTH = 500; // search queries, reasons, subjects
|
|
61
|
+
export const MAX_MEDIUM_TEXT_LENGTH = 1000; // notes, paths, config values
|
|
62
|
+
export const MAX_URL_LENGTH = 2000; // URLs, tokens, long identifiers
|
|
63
|
+
export const MAX_LONG_TEXT_LENGTH = 10000; // descriptions, content, messages
|
|
64
|
+
export const MAX_RICH_TEXT_LENGTH = 50000; // HTML content, rich text
|
|
65
|
+
|
|
66
|
+
// ── Domain-specific field limits ─────────────────────────────────────────────
|
|
67
|
+
export const MAX_DISPLAY_NAME_LENGTH = 100;
|
|
68
|
+
export const MAX_FULL_NAME_LENGTH = 100;
|
|
69
|
+
export const MAX_BIO_LENGTH = 1000;
|
|
70
|
+
export const MAX_EMAIL_LENGTH = 320; // Based on RFC 5321 (64 for local-part + 1 for @ + 255 for domain)
|
|
71
|
+
|
|
72
|
+
// Payload limits
|
|
73
|
+
export const MAX_PAYLOAD_SIZE = 512 * 1024; // 512KB
|
|
74
|
+
export const MAX_REQUEST_BODY_BYTES = 512 * 1024; // 512KB — global limit for non-file-upload API routes
|
|
75
|
+
export const MAX_TEXT_FIELD_BYTES = 64000;
|
|
76
|
+
// 40KB — covers 10K emoji chars (4 bytes each)
|
|
77
|
+
export const MAX_WORKSPACE_NAME_LENGTH = 100;
|
|
78
|
+
export const MAX_TASK_NAME_LENGTH = 1024;
|
|
79
|
+
export const MAX_TASK_DESCRIPTION_LENGTH = 100000;
|
|
80
|
+
export const MAX_CHAT_MESSAGE_LENGTH = 10000;
|
|
81
|
+
export const MAX_SUPPORT_INQUIRY_LENGTH = 5000;
|
|
82
|
+
export const MAX_CALENDAR_EVENT_TITLE_LENGTH = 255;
|
|
83
|
+
export const MAX_CALENDAR_EVENT_DESCRIPTION_LENGTH = 10000;
|
|
84
|
+
export const MAX_NOTE_TITLE_LENGTH = 255;
|
|
85
|
+
export const MAX_NOTE_CONTENT_LENGTH = 50000;
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function generateSalt() {
|
|
4
|
+
return crypto.randomBytes(10).toString('hex');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function hashPassword(password: string, salt: string) {
|
|
8
|
+
// concatenate the password and salt
|
|
9
|
+
const passwordWithSalt = password + salt;
|
|
10
|
+
|
|
11
|
+
// use native crypto to hash the password
|
|
12
|
+
const hashedPassword = await crypto.subtle.digest(
|
|
13
|
+
'SHA-256',
|
|
14
|
+
new TextEncoder().encode(passwordWithSalt)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// convert the hashed password to a hex string
|
|
18
|
+
return Array.from(new Uint8Array(hashedPassword))
|
|
19
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
20
|
+
.join('');
|
|
21
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized currency configuration
|
|
3
|
+
*
|
|
4
|
+
* This module provides a single source of truth for all supported currencies.
|
|
5
|
+
* The `SupportedCurrency` type is automatically derived from the array,
|
|
6
|
+
* ensuring type safety without manual maintenance of union types.
|
|
7
|
+
*
|
|
8
|
+
* To add a new currency:
|
|
9
|
+
* 1. Add an entry to SUPPORTED_CURRENCIES below
|
|
10
|
+
* 2. Add translations in apps/web/messages/{en,vi}.json under ws-finance-settings
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* All supported currencies with their locale mappings.
|
|
15
|
+
* Sorted alphabetically by currency code.
|
|
16
|
+
*/
|
|
17
|
+
export const SUPPORTED_CURRENCIES = [
|
|
18
|
+
{ code: 'AED', locale: 'ar-AE', name: 'UAE Dirham' },
|
|
19
|
+
{ code: 'AUD', locale: 'en-AU', name: 'Australian Dollar' },
|
|
20
|
+
{ code: 'BRL', locale: 'pt-BR', name: 'Brazilian Real' },
|
|
21
|
+
{ code: 'CAD', locale: 'en-CA', name: 'Canadian Dollar' },
|
|
22
|
+
{ code: 'CHF', locale: 'de-CH', name: 'Swiss Franc' },
|
|
23
|
+
{ code: 'CNY', locale: 'zh-CN', name: 'Chinese Yuan' },
|
|
24
|
+
{ code: 'CZK', locale: 'cs-CZ', name: 'Czech Koruna' },
|
|
25
|
+
{ code: 'DKK', locale: 'da-DK', name: 'Danish Krone' },
|
|
26
|
+
{ code: 'EUR', locale: 'de-DE', name: 'Euro' },
|
|
27
|
+
{ code: 'GBP', locale: 'en-GB', name: 'British Pound' },
|
|
28
|
+
{ code: 'HKD', locale: 'zh-HK', name: 'Hong Kong Dollar' },
|
|
29
|
+
{ code: 'HUF', locale: 'hu-HU', name: 'Hungarian Forint' },
|
|
30
|
+
{ code: 'IDR', locale: 'id-ID', name: 'Indonesian Rupiah' },
|
|
31
|
+
{ code: 'INR', locale: 'hi-IN', name: 'Indian Rupee' },
|
|
32
|
+
{ code: 'JPY', locale: 'ja-JP', name: 'Japanese Yen' },
|
|
33
|
+
{ code: 'KRW', locale: 'ko-KR', name: 'South Korean Won' },
|
|
34
|
+
{ code: 'MXN', locale: 'es-MX', name: 'Mexican Peso' },
|
|
35
|
+
{ code: 'MYR', locale: 'ms-MY', name: 'Malaysian Ringgit' },
|
|
36
|
+
{ code: 'NOK', locale: 'nb-NO', name: 'Norwegian Krone' },
|
|
37
|
+
{ code: 'NZD', locale: 'en-NZ', name: 'New Zealand Dollar' },
|
|
38
|
+
{ code: 'PHP', locale: 'en-PH', name: 'Philippine Peso' },
|
|
39
|
+
{ code: 'PLN', locale: 'pl-PL', name: 'Polish Zloty' },
|
|
40
|
+
{ code: 'SAR', locale: 'ar-SA', name: 'Saudi Riyal' },
|
|
41
|
+
{ code: 'SEK', locale: 'sv-SE', name: 'Swedish Krona' },
|
|
42
|
+
{ code: 'SGD', locale: 'en-SG', name: 'Singapore Dollar' },
|
|
43
|
+
{ code: 'THB', locale: 'th-TH', name: 'Thai Baht' },
|
|
44
|
+
{ code: 'TWD', locale: 'zh-TW', name: 'Taiwan Dollar' },
|
|
45
|
+
{ code: 'USD', locale: 'en-US', name: 'US Dollar' },
|
|
46
|
+
{ code: 'VND', locale: 'vi-VN', name: 'Vietnamese Dong' },
|
|
47
|
+
{ code: 'ZAR', locale: 'en-ZA', name: 'South African Rand' },
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Type representing all supported currency codes.
|
|
52
|
+
* Automatically derived from SUPPORTED_CURRENCIES array.
|
|
53
|
+
*/
|
|
54
|
+
export type SupportedCurrency = (typeof SUPPORTED_CURRENCIES)[number]['code'];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Type for a single currency configuration entry.
|
|
58
|
+
*/
|
|
59
|
+
export type CurrencyConfig = (typeof SUPPORTED_CURRENCIES)[number];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the locale for a specific currency.
|
|
63
|
+
* Falls back to 'en-US' for unknown currencies.
|
|
64
|
+
*
|
|
65
|
+
* @param currency - The currency code (e.g., 'USD', 'EUR', 'VND')
|
|
66
|
+
* @returns The BCP 47 locale string for the currency
|
|
67
|
+
*/
|
|
68
|
+
export function getCurrencyLocale(currency = 'VND'): string {
|
|
69
|
+
const found = SUPPORTED_CURRENCIES.find(
|
|
70
|
+
(c) => c.code === currency.toUpperCase()
|
|
71
|
+
);
|
|
72
|
+
return found?.locale ?? 'en-US';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a currency code is supported.
|
|
77
|
+
*
|
|
78
|
+
* @param currency - The currency code to check
|
|
79
|
+
* @returns True if the currency is supported
|
|
80
|
+
*/
|
|
81
|
+
export function isSupportedCurrency(
|
|
82
|
+
currency: string
|
|
83
|
+
): currency is SupportedCurrency {
|
|
84
|
+
return SUPPORTED_CURRENCIES.some((c) => c.code === currency.toUpperCase());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the full configuration for a currency.
|
|
89
|
+
*
|
|
90
|
+
* @param currency - The currency code
|
|
91
|
+
* @returns The currency configuration or undefined if not found
|
|
92
|
+
*/
|
|
93
|
+
export function getCurrencyConfig(
|
|
94
|
+
currency: string
|
|
95
|
+
): CurrencyConfig | undefined {
|
|
96
|
+
return SUPPORTED_CURRENCIES.find((c) => c.code === currency.toUpperCase());
|
|
97
|
+
}
|