All files / src/views SyncApplyView.vue

93.61% Statements 44/47
80% Branches 28/35
83.33% Functions 5/6
93.47% Lines 43/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153                    10x 10x 10x 10x 10x   10x 10x 10x 10x 10x   10x 10x 10x   4x   10x         1x 1x 1x 1x     1x 1x   1x 1x       1x           22x       30x       1x 1x 1x     1x                           1x   1x 1x               1x     1x                                                   1x 16x         1x 1x 1x 1x         1x                 1x                  
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { shareService } from '../services/share'
import { useListsStore } from '../stores/lists'
import { useProfileStore } from '../stores/profile'
import { useThemeStore } from '../stores/theme'
import { getUserId } from '../services/userId'
import type { SyncPreviewResponse } from '../types'
 
const route = useRoute()
const router = useRouter()
const listsStore = useListsStore()
const profileStore = useProfileStore()
const themeStore = useThemeStore()
 
const token = route.params.token as string
const preview = ref<SyncPreviewResponse | null>(null)
const loading = ref(true)
const applying = ref(false)
const error = ref<'not_found' | 'expired' | null>(null)
 
onMounted(async () => {
  try {
    preview.value = await shareService.previewSyncToken(token)
  } catch (e: any) {
    error.value = e?.response?.status === 410 ? 'expired' : 'not_found'
  } finally {
    loading.value = false
  }
})
 
async function apply() {
  applying.value = true
  try {
    const result = await shareService.applySyncToken(token)
    Iif (result.displayName || result.profilePicture) {
      profileStore.applyFromSync(result.displayName, result.profilePicture)
    }
    Eif (result.theme) {
      themeStore.theme = result.theme as 'dark' | 'light'
    }
    await listsStore.fetchAll()
    router.push({ name: 'home', params: { userId: getUserId()! } })
  } catch (e: any) {
    error.value = e?.response?.status === 410 ? 'expired' : 'not_found'
  } finally {
    applying.value = false
  }
}
</script>
 
<template>
  <div class="min-h-screen bg-ctp-base flex flex-col items-center justify-center px-6 py-16">
 
    <!-- Loading -->
    <div v-if="loading" class="flex flex-col items-center gap-4 w-full max-w-sm">
      <div v-for="n in 3" :key="n" class="h-16 w-full bg-ctp-surface0 rounded-xl skeleton" />
    </div>
 
    <!-- Error state -->
    <div v-else-if="error" class="flex flex-col items-center gap-4 text-center animate-fade-up">
      <span class="text-5xl">{{ error === 'expired' ? '⏳' : '❌' }}</span>
      <h2 class="text-xl font-bold text-ctp-text">
        {{ error === 'expired' ? 'Link abgelaufen' : 'Link ungültig' }}
      </h2>
      <p class="text-sm text-ctp-subtext0 max-w-xs">
        {{ error === 'expired'
          ? 'Sync-Links sind 30 Tage gültig. Bitte erstelle einen neuen Link auf dem anderen Gerät.'
          : 'Dieser Sync-Link wurde nicht gefunden.' }}
      </p>
      <button
        @click="router.push({ name: 'home', params: { userId: getUserId()! } })"
        class="mt-2 px-6 py-2.5 bg-ctp-surface0 text-ctp-text rounded-xl font-medium text-sm"
      >
        Zur Startseite
      </button>
    </div>
 
    <!-- Preview + apply -->
    <div v-else-if="preview" class="flex flex-col gap-6 w-full max-w-sm animate-fade-up">
      <!-- Source identity -->
      <div class="text-center">
        <div class="flex items-center justify-center mb-3">
          <div v-if="preview.sourceProfilePicture" class="w-16 h-16 rounded-full overflow-hidden border-2 border-ctp-surface1">
            <img :src="preview.sourceProfilePicture" alt="Profilbild" class="w-full h-full object-cover" />
          </div>
          <div v-else class="w-16 h-16 rounded-full bg-ctp-surface0 border-2 border-ctp-surface1 flex items-center justify-center text-2xl">
            👤
          </div>
        </div>
        <h2 class="text-xl font-bold text-ctp-text">
          {{ preview.sourceDisplayName ? `${preview.sourceDisplayName}s Gerät verknüpfen` : 'Gerät verknüpfen' }}
        </h2>
        <p class="text-sm text-ctp-subtext0 mt-1">
          {{ preview.lists.length }} {{ preview.lists.length === 1 ? 'Liste wird' : 'Listen werden' }} auf dieses Gerät übertragen
        </p>
      </div>
 
      <!-- What gets synced -->
      <div class="bg-ctp-surface0/40 border border-ctp-surface1/50 rounded-xl px-4 py-3 space-y-1.5 text-sm text-ctp-subtext1">
        <div class="flex items-center gap-2">
          <span>✅</span>
          <span>Alle Listen & Einträge</span>
        </div>
        <div class="flex items-center gap-2">
          <span>✅</span>
          <span>Profilbild & Name</span>
        </div>
        <div class="flex items-center gap-2">
          <span>✅</span>
          <span>Vorlagen (Presets)</span>
        </div>
        <div class="flex items-center gap-2">
          <span>✅</span>
          <span>Design-Thema</span>
        </div>
      </div>
 
      <!-- List preview -->
      <div class="space-y-2">
        <div
          v-for="list in preview.lists"
          :key="list.id"
          class="flex items-center gap-3 bg-ctp-surface0/60 border border-ctp-surface1/50 rounded-xl px-4 py-3"
        >
          <span class="text-xl">{{ list.emoji }}</span>
          <div class="flex-1 min-w-0">
            <p class="font-medium text-ctp-text truncate">{{ list.name }}</p>
            <p class="text-xs text-ctp-subtext0">{{ list.itemCount }} Items</p>
          </div>
        </div>
      </div>
 
      <div class="flex flex-col gap-2">
        <button
          @click="apply"
          :disabled="applying"
          class="w-full py-3 bg-linear-to-r from-ctp-teal to-ctp-sapphire text-ctp-base font-semibold rounded-xl disabled:opacity-60 transition-opacity"
        >
          {{ applying ? 'Importiere…' : 'Alle Listen importieren' }}
        </button>
        <button
          @click="router.push({ name: 'home', params: { userId: getUserId()! } })"
          class="w-full py-2.5 text-ctp-subtext0 text-sm rounded-xl hover:text-ctp-text transition-colors"
        >
          Abbrechen
        </button>
      </div>
    </div>
  </div>
</template>