Skip to content

Commit 1bc18b2

Browse files
committed
Add empty state for failed fetch
1 parent f653f30 commit 1bc18b2

File tree

8 files changed

+230
-48
lines changed

8 files changed

+230
-48
lines changed

src/assets/main.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,11 @@ button, a {
6767
height: 100%;
6868
border: 1px solid var(--app-border);
6969
@include borderRadiusOverflowHidden(8px);
70+
}
71+
72+
.icon {
73+
stroke: black;
74+
stroke-width: 1px;
75+
stroke-linejoin: round;
76+
paint-order: stroke;
7077
}

src/components/AppButton.vue

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,138 @@
1+
<script lang="ts" setup>
2+
interface Props {
3+
loading?: boolean
4+
}
5+
interface Emits {
6+
(name: 'click', event: MouseEvent): void
7+
}
8+
9+
const props = withDefaults(defineProps<Props>(), {
10+
loading: false,
11+
})
12+
const emit = defineEmits<Emits>()
13+
14+
function handleClick(e: MouseEvent) {
15+
if (props.loading)
16+
return
17+
18+
emit('click', e)
19+
}
20+
</script>
21+
122
<template>
2-
<button class="button">
3-
<slot />
23+
<button
24+
:disabled="loading || undefined"
25+
class="button"
26+
:class="{ 'button-loading': loading }"
27+
@click="handleClick"
28+
>
29+
<div
30+
v-if="loading"
31+
class="button-loading-indicator"
32+
>
33+
<div class="button-loading-indicator-outer" />
34+
<div class="button-loading-indicator-inner" />
35+
</div>
36+
37+
<div class="button-content">
38+
<slot />
39+
</div>
440
</button>
541
</template>
642

743
<style lang="scss" scoped>
844
.button {
45+
height: 35px;
946
font-size: 14px;
1047
padding: 8px 12px;
1148
border: 1px solid var(--item-border-color);
1249
outline: none;
1350
background-color: var(--item-bg);
51+
position: relative;
52+
overflow: hidden;
1453
color: var(--white);
1554
border-radius: 8px;
16-
@include focus-visible;
1755
@include text-outline();
1856
19-
&:active {
20-
background-color: var(--item-hover-bg);
57+
&-loading {
58+
.button-content {
59+
opacity: 0;
60+
}
61+
}
62+
63+
&:not(.button-loading) {
64+
@include focus-visible;
65+
66+
&:active {
67+
background-color: var(--item-hover-bg);
68+
}
69+
70+
&:hover {
71+
border-color: var(--item-hover-bg);;
72+
}
73+
}
74+
}
75+
76+
.button-loading-indicator {
77+
left: 0;
78+
top: 0;
79+
width: 100%;
80+
height: 100%;
81+
display: flex;
82+
align-items: center;
83+
justify-content: center;
84+
position: absolute;
85+
86+
$animation-cubic-bezier: cubic-bezier(.57,-0.66,.48,1.66);
87+
88+
&-inner {
89+
animation: .75s button-loading-inner-anim $animation-cubic-bezier infinite alternate-reverse;
90+
width: 10px;
91+
height: 10px;
92+
border-radius: 50%;
93+
background-color: var(--white);
94+
position: relative;
95+
}
96+
97+
&-outer {
98+
animation: 1.5s button-loading-outer-anim $animation-cubic-bezier infinite;
99+
position: absolute;
100+
left: 0;
101+
top: 0;
102+
right: 0;
103+
bottom: 0;
104+
margin: auto;
105+
width: 25px;
106+
height: 25px;
107+
border-radius: 50%;
108+
border: 2px solid var(--app-border);
109+
}
110+
}
111+
112+
@keyframes button-loading-outer-anim {
113+
0% {
114+
transform: scale(0.25);
115+
opacity: 1;
116+
}
117+
118+
25% {
119+
opacity: 1;
120+
}
121+
122+
50%,
123+
100% {
124+
opacity: 0;
125+
transform: scale(1);
126+
}
127+
}
128+
129+
@keyframes button-loading-inner-anim {
130+
0% {
131+
transform: scale(1);
21132
}
22133
23-
&:hover {
24-
border-color: var(--item-hover-bg);;
134+
100% {
135+
transform: scale(0.5);
25136
}
26137
}
27138
</style>

src/components/EmptyState.vue

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,68 @@
1-
<template />
1+
<script lang="ts">
2+
import type { Option } from '../types'
3+
import { type IconComponent } from './Icons'
4+
import PageHeader from './PageHeader.vue'
5+
6+
export enum EmptyStateIconSize {
7+
Regular,
8+
Big,
9+
}
10+
</script>
11+
12+
<script lang="ts" setup>
13+
interface Props {
14+
icon?: Option<IconComponent>
15+
description: string
16+
iconSize?: Option<EmptyStateIconSize>
17+
}
18+
withDefaults(defineProps<Props>(), {
19+
iconSize: EmptyStateIconSize.Regular,
20+
})
21+
</script>
22+
23+
<template>
24+
<div class="empty-state">
25+
<Component
26+
:is="icon"
27+
v-if="icon != null"
28+
class="empty-state-icon"
29+
:class="{ 'empty-state-icon-big': iconSize === EmptyStateIconSize.Big }"
30+
/>
31+
32+
<PageHeader>
33+
{{ description }}
34+
</PageHeader>
35+
36+
<div class="empty-state-footer">
37+
<slot name="footer" />
38+
</div>
39+
</div>
40+
</template>
41+
42+
<style lang="scss" scoped>
43+
.empty-state {
44+
width: 100%;
45+
height: 100%;
46+
display: flex;
47+
flex-flow: column nowrap;
48+
padding: 25px;
49+
align-items: center;
50+
justify-content: center;
51+
52+
&-footer {
53+
width: 100%;
54+
text-align: center;
55+
margin-top: 15px;
56+
}
57+
58+
&-icon {
59+
font-size: 30px;
60+
color: var(--white);
61+
margin-bottom: 15px;
62+
63+
&-big {
64+
font-size: 50px;
65+
}
66+
}
67+
}
68+
</style>

src/components/Icons.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import MailIcon from 'virtual:icons/octicon/mail-24'
2525
import SignOutIcon16 from 'virtual:icons/octicon/sign-out-16'
2626
import ChevronLeftIcon from 'virtual:icons/octicon/chevron-left'
2727

28+
export type IconComponent = typeof Icons[keyof typeof Icons]
29+
2830
export const Icons = {
2931
More: markRaw(MoreIcon),
3032
Gear: markRaw(GearIcon),

src/components/SidebarButton.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ const props = withDefaults(defineProps<Props>(), {
4848
4949
:deep(.icon) {
5050
font-size: 16px;
51-
stroke: black;
52-
stroke-width: 1px;
53-
stroke-linejoin: round;
54-
paint-order: stroke;
5551
}
5652
}
5753
</style>

src/pages/HomePage.vue

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import NotificationSkeleton from '../components/NotificationSkeleton.vue'
1414
import { Page } from '../constants'
1515
import type { Option } from '../types'
1616
import { useElementNavigation } from '../composables/useElementNavigation'
17+
import EmptyState, { EmptyStateIconSize } from '../components/EmptyState.vue'
18+
import { Icons } from '../components/Icons'
19+
import AppButton from '../components/AppButton.vue'
1720
1821
const store = useStore()
1922
@@ -46,6 +49,26 @@ useElementNavigation({
4649
>
4750
<NotificationSkeleton v-if="store.skeletonVisible" />
4851

52+
<EmptyState
53+
v-else-if="store.failedLoadingNotifications"
54+
:iconSize="EmptyStateIconSize.Big"
55+
:icon="Icons.X"
56+
description="Oopsie! Couldn't load notifications."
57+
>
58+
<template #footer>
59+
<AppButton @click="store.fetchNotifications(true)">
60+
Refresh
61+
</AppButton>
62+
</template>
63+
</EmptyState>
64+
65+
<EmptyState
66+
v-else-if="store.notifications.length === 0"
67+
:iconSize="EmptyStateIconSize.Big"
68+
:icon="Icons.Check"
69+
description="It's all clear sir!"
70+
/>
71+
4972
<template v-else>
5073
<NotificationList
5174
v-for="notification of store.notifications"

src/pages/LandingPage.vue

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getAccessToken } from '../api/token'
1111
import { GITHUB_AUTH_URL } from '../api/constants'
1212
import { AppStorage } from '../storage'
1313
import { getUser } from '../api/user'
14+
import EmptyState from '../components/EmptyState.vue'
1415
1516
const store = useStore()
1617
@@ -46,43 +47,14 @@ function handleLogin() {
4647
</script>
4748

4849
<template>
49-
<div class="landing">
50-
<Icons.Github class="github-icon" />
51-
52-
<PageHeader class="landing-header">
53-
Welcome to Gitification
54-
</PageHeader>
55-
56-
<div class="landing-content">
50+
<EmptyState
51+
:icon="Icons.Github"
52+
description="Welcome to Gitification"
53+
>
54+
<template #footer>
5755
<AppButton @click="handleLogin">
5856
Log in via Github
5957
</AppButton>
60-
</div>
61-
</div>
58+
</template>
59+
</EmptyState>
6260
</template>
63-
64-
<style lang="scss" scoped>
65-
.landing {
66-
width: 100%;
67-
height: 100%;
68-
display: flex;
69-
flex-flow: column nowrap;
70-
padding: 25px;
71-
align-items: center;
72-
justify-content: center;
73-
74-
&-header {
75-
margin: 15px 0;
76-
}
77-
78-
&-content {
79-
width: 100%;
80-
text-align: center;
81-
}
82-
83-
.github-icon {
84-
font-size: 30px;
85-
color: var(--white);
86-
}
87-
}
88-
</style>

src/stores/store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { notificationListFromThreads } from '../utils/notification'
99
export const useStore = defineStore('store', () => {
1010
const notifications = ref<NotificationListData[]>([])
1111
const loadingNotifications = ref(false)
12+
const failedLoadingNotifications = ref(false)
1213
const skeletonVisible = ref(false)
1314

1415
async function fetchNotifications(withSkeletons = false) {
@@ -24,6 +25,7 @@ export const useStore = defineStore('store', () => {
2425
skeletonVisible.value = true
2526

2627
loadingNotifications.value = true
28+
failedLoadingNotifications.value = false
2729

2830
try {
2931
const { data } = await getNotifications({
@@ -37,6 +39,7 @@ export const useStore = defineStore('store', () => {
3739
catch (error) {
3840
console.error('NotificationError: ', error)
3941
notifications.value = []
42+
failedLoadingNotifications.value = true
4043
}
4144
finally {
4245
loadingNotifications.value = false
@@ -68,6 +71,7 @@ export const useStore = defineStore('store', () => {
6871
loadingNotifications,
6972
skeletonVisible,
7073
pageFrom,
74+
failedLoadingNotifications,
7175
fetchNotifications,
7276
logout,
7377
}

0 commit comments

Comments
 (0)