@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.
Files changed (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +122 -3
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Score calculation utility functions for Nova
3
+ *
4
+ * This module provides standardized functions for calculating scores
5
+ * across the Nova platform to ensure consistency.
6
+ */
7
+
8
+ export interface ScoreInput {
9
+ total_tests?: number | null;
10
+ passed_tests?: number | null;
11
+ total_criteria?: number | null;
12
+ sum_criterion_score?: number | null;
13
+ }
14
+
15
+ /**
16
+ * Calculates a standardized score based on test cases and criteria evaluation
17
+ *
18
+ * The score is calculated as follows:
19
+ * 1. If both tests and criteria exist:
20
+ * - Test Score = (passed_tests/total_tests) * 10 * 0.5
21
+ * - Criteria Score = (sum_criterion_score/(total_criteria*10)) * 10 * 0.5
22
+ * 2. If only criteria exist:
23
+ * - Criteria Score = (sum_criterion_score/(total_criteria*10)) * 10 * 1.0
24
+ * 3. If only tests exist:
25
+ * - Test Score = (passed_tests/total_tests) * 10 * 1.0
26
+ *
27
+ * Each problem has a maximum score of 10 points
28
+ *
29
+ * @param submission Object containing test and criteria data
30
+ * @returns Calculated score (0-10)
31
+ */
32
+ export function calculateScore(submission: ScoreInput): number {
33
+ const totalTests = submission.total_tests || 0;
34
+ const passedTests = submission.passed_tests || 0;
35
+ const totalCriteria = submission.total_criteria || 0;
36
+ const sumCriterionScore = submission.sum_criterion_score || 0;
37
+
38
+ const hasCriteria = totalCriteria > 0;
39
+ const hasTests = totalTests > 0;
40
+
41
+ let criteriaScore = 0;
42
+ let testScore = 0;
43
+
44
+ // Calculate criteria score
45
+ if (hasCriteria) {
46
+ // Criteria weight is 0.5 if tests exist, 1.0 otherwise
47
+ const criteriaWeight = hasTests ? 0.5 : 1.0;
48
+ criteriaScore =
49
+ (sumCriterionScore / (totalCriteria * 10)) * 10 * criteriaWeight;
50
+ }
51
+
52
+ // Calculate test score
53
+ if (hasTests) {
54
+ // Test weight is 0.5 when both tests and criteria exist, 1.0 when only tests exist
55
+ const testWeight = hasCriteria ? 0.5 : 1.0;
56
+ testScore = (passedTests / totalTests) * 10 * testWeight;
57
+ }
58
+
59
+ return criteriaScore + testScore;
60
+ }
61
+
62
+ /**
63
+ * Calculates scores for multiple problems and returns the best score for each
64
+ *
65
+ * @param submissions Array of submission objects with problem IDs
66
+ * @returns Map of problem ID to best score
67
+ */
68
+ export function calculateBestScores(
69
+ submissions: Array<ScoreInput & { problem_id: string }>
70
+ ): Map<string, number> {
71
+ const bestScores = new Map<string, number>();
72
+
73
+ submissions.forEach((submission) => {
74
+ if (!submission.problem_id) return;
75
+
76
+ const score = calculateScore(submission);
77
+ const currentBest = bestScores.get(submission.problem_id) || 0;
78
+
79
+ if (score > currentBest) {
80
+ bestScores.set(submission.problem_id, score);
81
+ }
82
+ });
83
+
84
+ return bestScores;
85
+ }
86
+
87
+ /**
88
+ * Aggregates scores by challenge
89
+ *
90
+ * @param problemScores Map of problem ID to score
91
+ * @param problemChallengeMap Map of problem ID to challenge ID
92
+ * @returns Object mapping challenge IDs to their total scores
93
+ */
94
+ export function aggregateByChallenge(
95
+ problemScores: Map<string, number>,
96
+ problemChallengeMap: Map<string, string>
97
+ ): Record<string, number> {
98
+ const challengeScores: Record<string, number> = {};
99
+
100
+ problemScores.forEach((score, problemId) => {
101
+ const challengeId = problemChallengeMap.get(problemId);
102
+ if (challengeId) {
103
+ challengeScores[challengeId] =
104
+ (challengeScores[challengeId] || 0) + score;
105
+ }
106
+ });
107
+
108
+ return challengeScores;
109
+ }
110
+
111
+ /**
112
+ * Formats a score for display
113
+ *
114
+ * @param score Raw score number
115
+ * @param decimals Number of decimal places (default: 1)
116
+ * @returns Formatted score string
117
+ */
118
+ export function formatScore(score: number, decimals: number = 1): string {
119
+ return score.toFixed(decimals);
120
+ }
121
+
122
+ /**
123
+ * Calculates percentage based on score and maximum possible score
124
+ *
125
+ * @param score Current score
126
+ * @param maxScore Maximum possible score
127
+ * @returns Percentage (0-100)
128
+ */
129
+ export function calculatePercentage(score: number, maxScore: number): number {
130
+ if (maxScore <= 0) return 0;
131
+ return (score / maxScore) * 100;
132
+ }
@@ -0,0 +1,132 @@
1
+ import { createAdminClient } from '@tuturuuu/supabase/next/server';
2
+
3
+ export async function checkPermission({
4
+ problemId,
5
+ sessionId,
6
+ userId,
7
+ }: {
8
+ problemId: string;
9
+ sessionId: string | null;
10
+ userId: string;
11
+ }) {
12
+ const sbAdmin = await createAdminClient({ noCookie: true });
13
+
14
+ // Check if the user is an admin
15
+ const { data: roleData, error: roleError } = await sbAdmin
16
+ .from('platform_user_roles')
17
+ .select('*')
18
+ .eq('user_id', userId)
19
+ .eq('allow_challenge_management', true)
20
+ .single();
21
+
22
+ if (roleError && roleError.code !== 'PGRST116') {
23
+ console.error('Database Error when checking role:', roleError);
24
+ return {
25
+ canSubmit: false,
26
+ remainingAttempts: 0,
27
+ message: 'Error checking user permissions',
28
+ };
29
+ }
30
+
31
+ const isAdmin = roleData?.allow_challenge_management;
32
+
33
+ // Admin users can always submit without restrictions
34
+ if (isAdmin) {
35
+ return { canSubmit: true, remainingAttempts: -1, message: null };
36
+ }
37
+
38
+ // For non-admin users, validate session and submission count
39
+ if (!sessionId) {
40
+ return {
41
+ canSubmit: false,
42
+ remainingAttempts: 0,
43
+ message: 'sessionId is required for non-admin users',
44
+ };
45
+ }
46
+
47
+ // Check if the session is in progress
48
+ const { data: sessionData, error: sessionError } = await sbAdmin
49
+ .schema('private')
50
+ .from('nova_sessions')
51
+ .select('*')
52
+ .eq('id', sessionId)
53
+ .single();
54
+
55
+ if (sessionError) {
56
+ console.error('Database Error when checking session:', sessionError);
57
+ return {
58
+ canSubmit: false,
59
+ remainingAttempts: 0,
60
+ message: 'Error fetching session data',
61
+ };
62
+ }
63
+
64
+ const { data: challengeData, error: challengeError } = await sbAdmin
65
+ .schema('private')
66
+ .from('nova_challenges')
67
+ .select('duration, close_at')
68
+ .eq('id', sessionData.challenge_id)
69
+ .single();
70
+
71
+ if (challengeError || !challengeData) {
72
+ console.error('Database Error when checking challenge:', challengeError);
73
+ return {
74
+ canSubmit: false,
75
+ remainingAttempts: 0,
76
+ message: 'Error fetching challenge data',
77
+ };
78
+ }
79
+
80
+ const sessionEndTime = Math.min(
81
+ challengeData.close_at
82
+ ? new Date(challengeData.close_at).getTime()
83
+ : Infinity,
84
+ new Date(sessionData.start_time).getTime() + challengeData.duration * 1000
85
+ );
86
+
87
+ const currentTime = Date.now();
88
+
89
+ if (currentTime > sessionEndTime) {
90
+ return {
91
+ canSubmit: false,
92
+ remainingAttempts: 0,
93
+ message: 'Session has ended',
94
+ };
95
+ }
96
+
97
+ // Check submission count
98
+ const { error: countError, count } = await sbAdmin
99
+ .schema('private')
100
+ .from('nova_submissions')
101
+ .select('*', { count: 'exact', head: true })
102
+ .eq('problem_id', problemId)
103
+ .eq('session_id', sessionId)
104
+ .eq('user_id', userId);
105
+
106
+ if (countError) {
107
+ console.error('Database Error when counting submissions:', countError);
108
+ return {
109
+ canSubmit: false,
110
+ remainingAttempts: 0,
111
+ message: 'Error checking submission count',
112
+ };
113
+ }
114
+
115
+ const maxAttempts = 3;
116
+ const submissionCount = count || 0;
117
+ const remainingAttempts = maxAttempts - submissionCount;
118
+
119
+ if (submissionCount >= maxAttempts) {
120
+ return {
121
+ canSubmit: false,
122
+ remainingAttempts: 0,
123
+ message: 'You have reached the maximum of 3 submissions.',
124
+ };
125
+ }
126
+
127
+ return {
128
+ canSubmit: true,
129
+ remainingAttempts,
130
+ message: null,
131
+ };
132
+ }
@@ -0,0 +1,213 @@
1
+ import { createClient } from '@tuturuuu/supabase/next/client';
2
+
3
+ export interface OnboardingProgress {
4
+ user_id: string;
5
+ completed_steps: string[];
6
+ current_step: string;
7
+ workspace_name?: string | null;
8
+ workspace_description?: string | null;
9
+ workspace_avatar_url?: string | null;
10
+ profile_completed: boolean;
11
+ tour_completed: boolean;
12
+ completed_at?: string | null;
13
+ created_at: string;
14
+ updated_at: string;
15
+ }
16
+
17
+ export interface WhitelistStatus {
18
+ is_whitelisted: boolean;
19
+ enabled: boolean;
20
+ allow_challenge_management: boolean;
21
+ allow_manage_all_challenges: boolean;
22
+ allow_role_management: boolean;
23
+ }
24
+
25
+ export interface WorkspaceTemplate {
26
+ id: string;
27
+ name: string;
28
+ description?: string | null;
29
+ avatar_url?: string | null;
30
+ is_default: boolean;
31
+ created_at: string;
32
+ }
33
+
34
+ export const ONBOARDING_STEPS = {
35
+ WELCOME: 'welcome',
36
+ WORKSPACE_SETUP: 'workspace_setup',
37
+ PROFILE_COMPLETION: 'profile_completion',
38
+ FEATURE_TOUR: 'feature_tour',
39
+ DASHBOARD_REDIRECT: 'dashboard_redirect',
40
+ } as const;
41
+
42
+ export type OnboardingStep =
43
+ (typeof ONBOARDING_STEPS)[keyof typeof ONBOARDING_STEPS];
44
+
45
+ /**
46
+ * Check if a user is whitelisted to access the platform
47
+ */
48
+ export async function checkUserWhitelistStatus(
49
+ userId: string
50
+ ): Promise<WhitelistStatus> {
51
+ const supabase = createClient();
52
+
53
+ const { data, error } = await supabase
54
+ .rpc('get_user_whitelist_status', { user_id_param: userId })
55
+ .single();
56
+
57
+ if (error) {
58
+ console.error('Error checking whitelist status:', error);
59
+ return {
60
+ is_whitelisted: false,
61
+ enabled: false,
62
+ allow_challenge_management: false,
63
+ allow_manage_all_challenges: false,
64
+ allow_role_management: false,
65
+ };
66
+ }
67
+
68
+ return data;
69
+ }
70
+
71
+ /**
72
+ * Get user's onboarding progress
73
+ */
74
+ export async function getUserOnboardingProgress(
75
+ userId: string
76
+ ): Promise<OnboardingProgress | null> {
77
+ const supabase = createClient();
78
+
79
+ const { data, error } = await supabase
80
+ .from('onboarding_progress')
81
+ .select('*')
82
+ .eq('user_id', userId)
83
+ .single();
84
+
85
+ if (error) {
86
+ if (error.code === 'PGRST116') {
87
+ // No record found, return null
88
+ return null;
89
+ }
90
+ console.error('Error fetching onboarding progress:', error);
91
+ return null;
92
+ }
93
+
94
+ return data;
95
+ }
96
+
97
+ /**
98
+ * Create or update user's onboarding progress
99
+ */
100
+ export async function updateOnboardingProgress(
101
+ userId: string,
102
+ updates: Partial<
103
+ Omit<OnboardingProgress, 'user_id' | 'created_at' | 'updated_at'>
104
+ >
105
+ ): Promise<OnboardingProgress | null> {
106
+ const supabase = createClient();
107
+
108
+ const { data, error } = await supabase
109
+ .from('onboarding_progress')
110
+ .upsert(
111
+ {
112
+ user_id: userId,
113
+ ...updates,
114
+ },
115
+ {
116
+ onConflict: 'user_id',
117
+ }
118
+ )
119
+ .select()
120
+ .single();
121
+
122
+ if (error) {
123
+ console.error('Error updating onboarding progress:', error);
124
+ return null;
125
+ }
126
+
127
+ return data;
128
+ }
129
+
130
+ /**
131
+ * Mark an onboarding step as completed
132
+ */
133
+ export async function completeOnboardingStep(
134
+ userId: string,
135
+ step: OnboardingStep,
136
+ nextStep?: OnboardingStep
137
+ ): Promise<boolean> {
138
+ const progress = await getUserOnboardingProgress(userId);
139
+ const completedSteps = progress?.completed_steps || [];
140
+
141
+ if (!completedSteps.includes(step)) {
142
+ completedSteps.push(step);
143
+ }
144
+
145
+ const updates: Partial<OnboardingProgress> = {
146
+ completed_steps: completedSteps,
147
+ current_step: nextStep || step,
148
+ };
149
+
150
+ // Check if all steps are completed
151
+ const allSteps = Object.values(ONBOARDING_STEPS);
152
+ const isCompleted = allSteps.every((s) => completedSteps.includes(s));
153
+
154
+ if (isCompleted) {
155
+ updates.completed_at = new Date().toISOString();
156
+ }
157
+
158
+ const result = await updateOnboardingProgress(userId, updates);
159
+ return !!result;
160
+ }
161
+
162
+ /**
163
+ * Check if user has completed onboarding
164
+ */
165
+ export async function hasCompletedOnboarding(userId: string): Promise<boolean> {
166
+ const progress = await getUserOnboardingProgress(userId);
167
+ return !!progress?.completed_at;
168
+ }
169
+
170
+ /**
171
+ * Create workspace from onboarding data
172
+ */
173
+ export async function createWorkspaceFromOnboarding(
174
+ userId: string,
175
+ workspaceName: string,
176
+ avatarUrl?: string
177
+ ): Promise<{ success: boolean; workspaceId?: string; error?: string }> {
178
+ const supabase = createClient();
179
+
180
+ try {
181
+ // Create the workspace
182
+ const { data: workspace, error: workspaceError } = await supabase
183
+ .from('workspaces')
184
+ .insert({
185
+ name: workspaceName,
186
+ avatar_url: avatarUrl,
187
+ creator_id: userId,
188
+ })
189
+ .select('id')
190
+ .single();
191
+
192
+ if (workspaceError) {
193
+ return { success: false, error: workspaceError.message };
194
+ }
195
+
196
+ // Set as user's default workspace
197
+ const { error: updateError } = await supabase
198
+ .from('user_private_details')
199
+ .update({ default_workspace_id: workspace.id })
200
+ .eq('user_id', userId);
201
+
202
+ if (updateError) {
203
+ console.warn('Could not set default workspace:', updateError);
204
+ }
205
+
206
+ return { success: true, workspaceId: workspace.id };
207
+ } catch (error) {
208
+ return {
209
+ success: false,
210
+ error: error instanceof Error ? error.message : 'Unknown error',
211
+ };
212
+ }
213
+ }
@@ -0,0 +1,93 @@
1
+ export function joinPath(...paths: string[]) {
2
+ if (paths.length === 0) return '/';
3
+
4
+ // Clean up paths and filter empty ones
5
+ const validPaths = paths.filter(Boolean).map((p) => p.trim());
6
+ if (validPaths.length === 0) return '/';
7
+
8
+ // Track path properties
9
+ const firstPath = validPaths[0];
10
+ const shouldBeRelative =
11
+ firstPath?.startsWith('./') || firstPath?.startsWith('../');
12
+ const shouldStartWithSlash =
13
+ firstPath?.startsWith('/') || firstPath?.startsWith('//');
14
+ const shouldHaveTrailingSlash = paths[paths.length - 1]?.endsWith('/');
15
+
16
+ // Process each path segment
17
+ const segments: string[] = [];
18
+ for (const path of validPaths) {
19
+ const parts = path.split('/');
20
+ for (const part of parts) {
21
+ if (part === '.') {
22
+ continue; // Skip "." segments
23
+ }
24
+ if (part) {
25
+ segments.push(part);
26
+ }
27
+ }
28
+ }
29
+
30
+ let result = segments.join('/');
31
+
32
+ // Handle special cases
33
+ if (!result || result === '/') return '/';
34
+
35
+ // Add leading slash for absolute paths
36
+ if (!shouldBeRelative && shouldStartWithSlash) {
37
+ result = `/${result.replace(/^\//, '')}`;
38
+ }
39
+
40
+ // Add leading './' if the first path started with './'
41
+ if (shouldBeRelative && firstPath?.startsWith('./')) {
42
+ result = `./${result}`;
43
+ }
44
+
45
+ // Add trailing slash if original had one
46
+ if (shouldHaveTrailingSlash && !result.endsWith('/')) {
47
+ result += '/';
48
+ }
49
+
50
+ return result;
51
+ }
52
+
53
+ export function popPath(path: string) {
54
+ if (!path) return '/';
55
+
56
+ // Clean and normalize the path
57
+ let cleanPath = path.trim().replace(/\/+/g, '/');
58
+ const isRelative = cleanPath.startsWith('./') || cleanPath.startsWith('../');
59
+ const isAbsolute = cleanPath.startsWith('/');
60
+
61
+ // Handle root and empty cases
62
+ if (!cleanPath || cleanPath === '/' || cleanPath === '.') return '/';
63
+
64
+ // Remove trailing slashes for processing
65
+ cleanPath = cleanPath.replace(/\/*$/, '');
66
+
67
+ // Find last slash
68
+ const lastSlash = cleanPath.lastIndexOf('/');
69
+
70
+ // No slash found
71
+ if (lastSlash === -1) {
72
+ if (isRelative) return '.';
73
+ return isAbsolute ? '/' : '/base';
74
+ }
75
+
76
+ // Get parent path
77
+ let result = cleanPath.slice(0, lastSlash);
78
+ if (!result) return '/';
79
+
80
+ // Handle relative paths
81
+ if (isRelative) {
82
+ if (!result.startsWith('./') && !result.startsWith('../')) {
83
+ result = `./${result.replace(/^\/+/, '')}`;
84
+ }
85
+ } else {
86
+ // Ensure leading slash for absolute paths and paths without protocol
87
+ if (!result.includes('://') && !result.startsWith('/')) {
88
+ result = `/${result}`;
89
+ }
90
+ }
91
+
92
+ return result;
93
+ }