@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,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
|
+
}
|