[feat] Add skeleton loading states to cloud onboarding flow (#5568)

* [feat] Add skeleton loading states to cloud onboarding flow

- Create dedicated skeleton components matching exact layouts
- CloudLoginViewSkeleton for login page with beta notice, form, social buttons
- CloudSurveyViewSkeleton for multi-step survey with progress bar
- CloudWaitlistViewSkeleton for waitlist page with title and messages
- CloudClaimInviteViewSkeleton for invite claiming page
- Update UserCheckView to show contextual skeleton based on redirect destination
- Update InviteCheckView to show appropriate skeleton during loading
- Use i18n for loading text to maintain consistency

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [feat] Fix skeleton loading flow to show progressive states

- Start with simple loading text when checking user status
- Show survey skeleton while checking survey completion
- Show waitlist skeleton while checking user activation status
- Show login skeleton when redirecting to login on error
- Preserve all original comments from upstream authors
- Use progressive disclosure based on API response flow

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-09-14 19:40:41 -07:00
committed by GitHub
parent 59a1380f39
commit a13eeaea7e
6 changed files with 147 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div />
<CloudClaimInviteViewSkeleton />
</template>
<script setup lang="ts">
@@ -8,6 +8,8 @@ import { useRoute, useRouter } from 'vue-router'
import { getInviteCodeStatus } from '@/api/auth'
import CloudClaimInviteViewSkeleton from './skeletons/CloudClaimInviteViewSkeleton.vue'
const router = useRouter()
const route = useRoute()

View File

@@ -1,5 +1,10 @@
<template>
<div />
<CloudLoginViewSkeleton v-if="skeletonType === 'login'" />
<CloudSurveyViewSkeleton v-else-if="skeletonType === 'survey'" />
<CloudWaitlistViewSkeleton v-else-if="skeletonType === 'waitlist'" />
<div v-else class="flex items-center justify-center min-h-screen">
<div class="animate-pulse text-gray-500">{{ $t('g.loading') }}</div>
</div>
</template>
<script setup lang="ts">
@@ -8,8 +13,13 @@ import { useRouter } from 'vue-router'
import { getSurveyCompletedStatus, getUserCloudStatus } from '@/api/auth'
import CloudLoginViewSkeleton from './skeletons/CloudLoginViewSkeleton.vue'
import CloudSurveyViewSkeleton from './skeletons/CloudSurveyViewSkeleton.vue'
import CloudWaitlistViewSkeleton from './skeletons/CloudWaitlistViewSkeleton.vue'
const router = useRouter()
const isNavigating = ref(false)
const skeletonType = ref<'login' | 'survey' | 'waitlist' | 'loading'>('loading')
onMounted(async () => {
// Prevent multiple executions
@@ -21,28 +31,37 @@ onMounted(async () => {
// Wait for next tick to ensure component is fully mounted
await nextTick()
const cloudUserStats = await getUserCloudStatus()
const surveyStatus = await getSurveyCompletedStatus()
try {
const cloudUserStats = await getUserCloudStatus()
if (!cloudUserStats) {
skeletonType.value = 'login'
await router.replace({ name: 'cloud-login' })
return
}
// We know user exists, now check survey status - show survey skeleton while loading
skeletonType.value = 'survey'
const surveyStatus = await getSurveyCompletedStatus()
// Check onboarding status and redirect accordingly
if (!surveyStatus) {
// User hasn't completed survey
await router.replace({ name: 'cloud-survey' })
} else if (cloudUserStats.status !== 'active') {
// User completed survey but not whitelisted
await router.replace({ name: 'cloud-waitlist' })
} else {
// User is fully onboarded - just reload the page to bypass router issues
window.location.href = '/'
// Survey is done, now check if waitlisted - show waitlist skeleton while loading
skeletonType.value = 'waitlist'
if (cloudUserStats.status !== 'active') {
// User completed survey but not whitelisted
await router.replace({ name: 'cloud-waitlist' })
} else {
// User is fully onboarded - just reload the page to bypass router issues
window.location.href = '/'
}
}
} catch (error) {
// On error, fallback to page reload
skeletonType.value = 'login'
await router.push({ name: 'cloud-login' })
}
})

View File

@@ -0,0 +1,12 @@
<template>
<div
class="flex flex-col justify-center items-center h-screen font-mono text-black gap-4"
>
<Skeleton width="60%" height="2rem" />
<Skeleton width="30%" height="2.5rem" />
</div>
</template>
<script setup lang="ts">
import Skeleton from 'primevue/skeleton'
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="h-full flex items-center justify-center p-8">
<div class="w-96 p-2">
<div class="bg-[#2d2e32] p-4 rounded-lg">
<Skeleton width="60%" height="1.125rem" class="mb-2" />
<Skeleton width="90%" height="1rem" class="mb-2" />
<Skeleton width="80%" height="1rem" />
</div>
<div class="flex flex-col gap-4 mt-6 mb-8">
<Skeleton width="45%" height="1.5rem" class="my-0" />
<div class="flex items-center">
<Skeleton width="25%" height="1rem" class="mr-1" />
<Skeleton width="20%" height="1rem" />
</div>
</div>
<div class="mb-8">
<Skeleton width="20%" height="1rem" class="mb-2" />
<Skeleton width="100%" height="2.5rem" class="mb-4" />
<Skeleton width="25%" height="1rem" class="mb-4" />
<Skeleton width="100%" height="2.5rem" class="mb-6" />
<Skeleton width="80%" height="1rem" class="mb-4" />
<Skeleton width="100%" height="2.5rem" />
</div>
<div class="flex items-center my-8">
<div class="flex-1 border-t border-gray-300"></div>
<Skeleton width="30%" height="1rem" class="mx-4" />
<div class="flex-1 border-t border-gray-300"></div>
</div>
<div class="flex flex-col gap-6">
<Skeleton width="100%" height="2.5rem" />
<Skeleton width="100%" height="2.5rem" />
</div>
<div class="mt-5">
<Skeleton width="70%" height="0.875rem" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Skeleton from 'primevue/skeleton'
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div>
<div class="flex flex-col min-h-[638px] min-w-[320px]">
<Skeleton width="100%" height="0.5rem" class="mb-8" />
<div class="p-0 flex-1 flex flex-col">
<div class="flex-1 min-h-full flex flex-col justify-between">
<div>
<Skeleton width="70%" height="1.75rem" class="mb-8" />
<div class="flex flex-col gap-6">
<div v-for="i in 5" :key="i" class="flex items-center gap-3">
<Skeleton width="1.25rem" height="1.25rem" shape="circle" />
<Skeleton width="85%" height="0.875rem" />
</div>
</div>
</div>
<div class="flex justify-between pt-4">
<span />
<Skeleton width="100%" height="2.5rem" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Skeleton from 'primevue/skeleton'
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="flex flex-col items-center justify-center p-8">
<div class="w-full max-w-md text-center">
<div class="mb-8">
<Skeleton width="80%" height="3rem" class="mb-2 mx-auto" />
<Skeleton width="60%" height="3rem" class="mx-auto" />
</div>
<div class="max-w-[320px] mx-auto">
<div class="mb-4">
<Skeleton width="90%" height="1.5rem" class="mb-2 mx-auto" />
<Skeleton width="70%" height="1.5rem" class="mx-auto" />
</div>
<div>
<Skeleton width="80%" height="1.5rem" class="mb-2 mx-auto" />
<div class="flex justify-center items-center gap-2">
<Skeleton width="20%" height="1.5rem" />
<Skeleton width="15%" height="1.5rem" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Skeleton from 'primevue/skeleton'
</script>