wip bsky client for the web & android
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: fix type errors

vi d5fa73ce cbc9cc16

+154 -86
+52 -76
src/components/UI/ListItem.vue
··· 1 1 <script setup lang="ts"> 2 2 import { computed } from 'vue' 3 3 import AppLink from '@/components/Navigation/AppLink.vue' 4 - import { IconChevronRightRounded } from '@iconify-prerendered/vue-material-symbols' 4 + import ListItemContent from './ListItemContent.vue' 5 + 6 + import type { PageNames, RouteParams } from '@/router' 5 7 6 8 const props = defineProps<{ 7 9 title?: string ··· 14 16 to?: string | object 15 17 target?: string 16 18 disabled?: boolean 19 + 20 + /** applink requires these */ 21 + name?: PageNames 22 + params?: RouteParams<PageNames> 17 23 }>() 18 24 19 25 const emit = defineEmits<{ 20 26 (e: 'click', event: MouseEvent): void 21 27 }>() 22 28 23 - const isLink = computed(() => !!props.href || !!props.to) 29 + const isNameLink = computed(() => !!props.name) 30 + const isHrefLink = computed(() => !!props.href) 31 + const isLink = computed(() => isNameLink.value || isHrefLink.value) 24 32 const isInteractive = computed(() => props.clickable || isLink.value) 25 33 26 - const componentType = computed(() => { 27 - if (props.to) return AppLink 28 - if (props.href) return 'a' 29 - return 'div' 30 - }) 31 - 32 34 function handleClick(e: MouseEvent) { 33 35 if (isInteractive.value) emit('click', e) 34 36 } ··· 43 45 </script> 44 46 45 47 <template> 46 - <component 47 - :is="componentType" 48 + <AppLink 49 + v-if="isNameLink && name" 50 + :name="name" 51 + :params="params" 48 52 class="list-item" 49 53 :class="{ 'is-clickable': isInteractive, 'is-danger': danger }" 50 - :href="href" 51 - :to="to" 52 - :target="target" 54 + @click="handleClick" 55 + @keydown="handleKeydown" 53 56 :tabindex="isInteractive ? 0 : -1" 54 57 :role="!isLink && isInteractive ? 'button' : undefined" 58 + > 59 + <ListItemContent :title="title" :subtitle="subtitle" :chevron="chevron"> 60 + <template #start><slot name="start" /></template> 61 + <slot /> 62 + <template #end><slot name="end" /></template> 63 + </ListItemContent> 64 + </AppLink> 65 + 66 + <a 67 + v-else-if="isHrefLink" 68 + :href="href" 69 + class="list-item" 70 + :class="{ 'is-clickable': isInteractive, 'is-danger': danger }" 55 71 @click="handleClick" 56 72 @keydown="handleKeydown" 73 + :tabindex="isInteractive ? 0 : -1" 74 + :role="!isLink && isInteractive ? 'button' : undefined" 75 + :target="target" 57 76 > 58 - <div v-if="$slots.start" class="item-start"> 59 - <slot name="start" /> 60 - </div> 61 - 62 - <div class="item-content"> 63 - <div v-if="title" class="item-title">{{ title }}</div> 64 - <div v-if="subtitle" class="item-subtitle">{{ subtitle }}</div> 77 + <ListItemContent :title="title" :subtitle="subtitle" :chevron="chevron"> 78 + <template #start><slot name="start" /></template> 65 79 <slot /> 66 - </div> 80 + <template #end><slot name="end" /></template> 81 + </ListItemContent> 82 + </a> 67 83 68 - <div v-if="$slots.end || chevron" class="item-end"> 69 - <slot name="end" /> 70 - <IconChevronRightRounded v-if="chevron" class="chevron" /> 71 - </div> 72 - </component> 84 + <div 85 + v-else 86 + class="list-item" 87 + :class="{ 'is-clickable': isInteractive, 'is-danger': danger }" 88 + @click="handleClick" 89 + @keydown="handleKeydown" 90 + :tabindex="isInteractive ? 0 : -1" 91 + :role="!isLink && isInteractive ? 'button' : undefined" 92 + > 93 + <ListItemContent :title="title" :subtitle="subtitle" :chevron="chevron"> 94 + <template #start><slot name="start" /></template> 95 + <slot /> 96 + <template #end><slot name="end" /></template> 97 + </ListItemContent> 98 + </div> 73 99 </template> 74 100 75 101 <style scoped lang="scss"> ··· 121 147 .chevron { 122 148 color: hsl(var(--red)); 123 149 } 124 - } 125 - 126 - .item-start { 127 - display: flex; 128 - align-items: center; 129 - justify-content: center; 130 - font-size: 1.5rem; 131 - color: hsl(var(--accent)); 132 - 133 - svg { 134 - display: block; 135 - } 136 - } 137 - 138 - .item-content { 139 - flex: 1; 140 - display: flex; 141 - flex-direction: column; 142 - justify-content: center; 143 - min-width: 0; 144 - gap: 2px; 145 - } 146 - 147 - .item-title { 148 - font-weight: 600; 149 - color: hsl(var(--text)); 150 - font-size: 1rem; 151 - white-space: nowrap; 152 - overflow: hidden; 153 - text-overflow: ellipsis; 154 - } 155 - 156 - .item-subtitle { 157 - font-size: 0.8rem; 158 - color: hsl(var(--subtext0)); 159 - line-height: 1.3; 160 - } 161 - 162 - .item-end { 163 - display: flex; 164 - align-items: center; 165 - gap: var(--space-2); 166 - color: hsl(var(--subtext1)); 167 - font-size: 0.9rem; 168 - font-weight: 500; 169 - } 170 - 171 - .chevron { 172 - font-size: 1.25rem; 173 - opacity: 0.4; 174 150 } 175 151 </style>
+89
src/components/UI/ListItemContent.vue
··· 1 + <script lang="ts" setup> 2 + import type { PageNames, RouteParams } from '@/router' 3 + 4 + defineProps<{ 5 + title?: string 6 + subtitle?: string 7 + chevron?: boolean 8 + clickable?: boolean 9 + danger?: boolean 10 + 11 + href?: string 12 + to?: string | object 13 + target?: string 14 + disabled?: boolean 15 + 16 + /** applink requires these */ 17 + name?: PageNames 18 + params?: RouteParams<PageNames> 19 + }>() 20 + </script> 21 + 22 + <template> 23 + <div v-if="$slots.start" class="item-start"> 24 + <slot name="start" /> 25 + </div> 26 + 27 + <div class="item-content"> 28 + <div v-if="title" class="item-title">{{ title }}</div> 29 + <div v-if="subtitle" class="item-subtitle">{{ subtitle }}</div> 30 + <slot /> 31 + </div> 32 + 33 + <div v-if="$slots.end || chevron" class="item-end"> 34 + <slot name="end" /> 35 + <IconChevronRightRounded v-if="chevron" class="chevron" /> 36 + </div> 37 + </template> 38 + 39 + <style lang="scss" scoped> 40 + .item-start { 41 + display: flex; 42 + align-items: center; 43 + justify-content: center; 44 + font-size: 1.5rem; 45 + color: hsl(var(--accent)); 46 + 47 + svg { 48 + display: block; 49 + } 50 + } 51 + 52 + .item-content { 53 + flex: 1; 54 + display: flex; 55 + flex-direction: column; 56 + justify-content: center; 57 + min-width: 0; 58 + gap: 2px; 59 + } 60 + 61 + .item-title { 62 + font-weight: 600; 63 + color: hsl(var(--text)); 64 + font-size: 1rem; 65 + white-space: nowrap; 66 + overflow: hidden; 67 + text-overflow: ellipsis; 68 + } 69 + 70 + .item-subtitle { 71 + font-size: 0.8rem; 72 + color: hsl(var(--subtext0)); 73 + line-height: 1.3; 74 + } 75 + 76 + .item-end { 77 + display: flex; 78 + align-items: center; 79 + gap: var(--space-2); 80 + color: hsl(var(--subtext1)); 81 + font-size: 0.9rem; 82 + font-weight: 500; 83 + } 84 + 85 + .chevron { 86 + font-size: 1.25rem; 87 + opacity: 0.4; 88 + } 89 + </style>
+3 -4
src/stores/navigation.ts
··· 211 211 return 212 212 } 213 213 214 - // @ts-expect-error: PopStateEvent's `state` is literally just `any` which 215 - // is annoying 216 214 const stack = stacks.value[tab] 217 - // @ts-expect-error: see above 215 + if (!stack) return // shouldnt happen ever 218 216 const currentIndex = stack.findIndex((entry) => entry.id === stackId) 219 217 220 218 if (currentIndex !== -1) { ··· 263 261 if (root) { 264 262 activeTab.value = page as TabKey 265 263 const stack = stacks.value[activeTab.value] 266 - if (!stack[0]) return 264 + if (!stack || !stack[0]) return 267 265 268 266 stack[0].props = props 269 267 updateHistory('replace', activeTab.value, stack[0]) ··· 280 278 activeTab.value = 'home' 281 279 282 280 const stack = stacks.value['home'] 281 + if (!stack) return 283 282 const rootEntry = stack[0] 284 283 if (!rootEntry) return 285 284
+10 -6
src/views/Auth/OAuthCallback.vue
··· 5 5 import BluebellLogo from '@/assets/icons/bluebell.svg?raw' 6 6 import { IconArrowForwardRounded } from '@iconify-prerendered/vue-material-symbols' 7 7 import Button from '@/components/UI/BaseButton.vue' 8 + import type { AppBskyActorGetProfile } from '@atcute/bluesky' 8 9 const emit = defineEmits<{ 9 10 (e: 'complete'): void 10 11 }>() ··· 14 15 type ViewState = 'loading' | 'success' | 'error' 15 16 const state = ref<ViewState>('loading') 16 17 const loadingMessage = ref('Connecting...') 17 - const profile = ref<any>(null) 18 + const profile = ref<AppBskyActorGetProfile.$output | null>(null) 18 19 const errorMsg = ref('') 19 20 const isExiting = ref(false) 20 21 ··· 36 37 37 38 onMounted(async () => { 38 39 try { 40 + if (!auth.session) return 39 41 const style = loadingBar.value?.style 40 42 41 43 loadingMessage.value = 'verifying... ' 42 - style?.setProperty('--scale', 1 / 3) 44 + style?.setProperty('--scale', (1 / 3).toString()) 43 45 44 46 const success = await auth.handleCallback() 45 47 if (!success && !auth.isAuthenticated) throw new Error('Auth failed') 46 48 47 49 loadingMessage.value = 'finding you...' 48 - style?.setProperty('--scale', 2 / 3) 50 + style?.setProperty('--scale', (2 / 3).toString()) 49 51 50 52 const rpc = auth.getRpc() 51 - const { data } = await rpc.get('app.bsky.actor.getProfile', { 53 + const { data, ok } = await rpc.get('app.bsky.actor.getProfile', { 52 54 params: { actor: auth.session?.info.sub }, 53 55 }) 56 + 57 + if (!ok) throw new Error(data.error) 54 58 55 59 if (data.banner) { 56 60 const img = new Image() ··· 63 67 64 68 await new Promise((r) => setTimeout(r, 750)) 65 69 state.value = 'success' 66 - } catch (e: any) { 70 + } catch (e) { 67 71 console.error(e) 68 72 state.value = 'error' 69 - errorMsg.value = e.message || 'Something went wrong' 73 + if (e instanceof Error) errorMsg.value = e.message 70 74 setTimeout(() => (window.location.href = '/login'), 3000) 71 75 } 72 76 })