Skip to content

Commit 52233e0

Browse files
committed
feat: add staking hub section to landing page
See: DF-97
1 parent f7454a7 commit 52233e0

File tree

14 files changed

+374
-31
lines changed

14 files changed

+374
-31
lines changed

frontend/i18n/locales/en.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,7 +1160,33 @@
11601160
"title": "Explore Transactions & Validator Staking on"
11611161
},
11621162
"staking_hub": {
1163+
"action": {
1164+
"go_to_stakinghub": "Go to StakingHub"
1165+
},
1166+
"cards": {
1167+
"notifications": {
1168+
"action": "Set up Notifications",
1169+
"description": "Set up alerts for what matters most — downtime, missed duties, rewards, and more. Get notified via email, push, or webhook, wherever you are.",
1170+
"subtitle": "Never miss a validator event again",
1171+
"title": "Notifications"
1172+
},
1173+
"staking_mobile_app": {
1174+
"action-download-appstore": "Download the Beaconchain Dashboard App on the App Store",
1175+
"action-download-playstore": "Download the Beaconchain Dashboard App on Play Store",
1176+
"description": "Let us help you with that.Easily check on your validators with our Beaconchain Dashboard app. Wherever you are, whenever you want.",
1177+
"subtitle": "Keeping the network secure is hard",
1178+
"title": "Staking Mobile App"
1179+
},
1180+
"validator_dashboards": {
1181+
"action": "Create your Dashboard",
1182+
"description": "Let us make it simple. Add your validators to a personal dashboard and get clear insights into rewards, attestations, and missed duties — all in one place.",
1183+
"subtitle": "Keeping track of your validators performance is tricky",
1184+
"title": "Validator Dashboards"
1185+
}
1186+
},
1187+
"description": "Stay ahead of your validators—get alerts for validator duties, and monitor their performance in real time.",
11631188
"title": "Comprehensive staking insights on"
1189+
},
11641190
}
11651191
},
11661192
"staking_hub": "StakingHub"

frontend/layers/base/app/assets/css/main.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
--breakpoint-sm: 40rem; /* 640px */
1515
--breakpoint-md: 48rem; /* 768px */
16-
/* --breakpoint-lg: 64rem; */
16+
--breakpoint-lg: 64rem; /* 1024px */
1717
/* --breakpoint-xl: 80rem; */
1818
/* --breakpoint-2xl: 96rem; */
1919

@@ -55,6 +55,7 @@
5555
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
5656

5757
--font-urbanist: "Urbanist", sans-serif;
58+
--font-weight-bold: 700;
5859
--font-weight-semibold: 600;
5960

6061
--radius-xl: .75rem; /* 12px */
@@ -86,7 +87,8 @@
8687
--text-md--line-height: 1.5rem; /* 24px */
8788
--text-lg: 1.125rem; /* 18px */
8889
--text-lg--line-height: 1.75rem; /* 28px */
89-
90+
--text-xl: 1.25rem; /* 20px */
91+
--text-xl--line-height: 1.5rem; /* 24px */
9092
--text-2xl: 1.5rem ; /* 24px */
9193
--text-2xl--line-height: 2rem; /* 32px */
9294

frontend/layers/base/app/components/BaseButton.vue

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
<script setup lang="ts">
2+
import { NuxtLink } from '#components'
3+
import type { NuxtLinkProps } from '#app'
24
import type { IconName } from '~/layers/base/app/components/BaseIcon.vue'
35
46
const {
57
size = 'md',
68
variant = 'primary',
7-
} = defineProps<{
8-
full?: boolean,
9-
leadingIcon?: IconName,
10-
size?: 'md' | 'xl',
11-
trailingIcon?: IconName,
12-
variant?: 'branded' | 'primary' | 'secondary',
13-
}>()
9+
} = defineProps<
10+
{
11+
full?: boolean,
12+
leadingIcon?: IconName,
13+
size?: 'md' | 'xl',
14+
trailingIcon?: IconName,
15+
variant?: 'branded' | 'primary' | 'secondary',
16+
}
17+
& (
18+
| { disabled?: boolean, to?: never }
19+
| { disabled?: never, to: NuxtLinkProps['to'] }
20+
)
21+
>()
1422
</script>
1523

1624
<template>
17-
<button
18-
class="flex justify-center items-center bg-linear-to-b rounded-full font-semibold disabled:opacity-40 aria-disabled:opacity-40 active:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-300"
25+
<component
26+
:is="to ? NuxtLink : 'button'"
27+
:to
28+
:disabled
29+
class="flex justify-center items-center bg-linear-to-b rounded-full font-semibold disabled:opacity-40 aria-disabled:opacity-40 aria-disabled:pointer-events-none active:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-300"
1930
:class="[
2031
variant === 'primary' && 'from-gray-700 to-gray-900 text-white dark:from-gray-100 dark:to-gray-300 dark:text-black opacity-90 hover:opacity-95',
2132
variant === 'secondary' && 'from-gray-300 to-gray-200 text-black dark:from-charcoal-600 dark:to-charcoal-700 dark:text-white opacity-90 hover:opacity-95',
@@ -38,5 +49,5 @@ const {
3849
:name="trailingIcon"
3950
class=""
4051
/>
41-
</button>
52+
</component>
4253
</template>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script setup lang="ts" generic="WORKAROUND_FOR_CONDITIONAL_PROPS">
2+
// https://github.com/vuejs/core/issues/8952
3+
import type { BaseHeadings } from '~/layers/base/app/components/BaseHeading.vue'
4+
import type { IconName } from '~/layers/base/app/components/BaseIcon.vue'
5+
6+
defineProps<
7+
{
8+
titleIcon?: IconName,
9+
} & (
10+
| {
11+
title: string,
12+
titleIs: BaseHeadings,
13+
}
14+
| {
15+
title?: never,
16+
titleIs?: never,
17+
}
18+
)
19+
>()
20+
</script>
21+
22+
<template>
23+
<div class="dark:bg-gray-950 p-3xl rounded-4xl flex flex-col gap-2xl">
24+
<div
25+
v-if="$slots.header || title"
26+
class="flex gap-md pb-3xl"
27+
>
28+
<slot name="header">
29+
<LazyBaseIcon
30+
v-if="titleIcon"
31+
:name="titleIcon"
32+
class="text-2xl"
33+
/>
34+
<LazyBaseHeading
35+
:is="titleIs"
36+
v-if="title && titleIs"
37+
size="xs"
38+
>
39+
{{ title }}
40+
</LazyBaseHeading>
41+
</slot>
42+
</div>
43+
<slot />
44+
<div
45+
v-if="$slots.footer"
46+
class="pt-3xl mt-auto"
47+
>
48+
<slot name="footer" />
49+
</div>
50+
</div>
51+
</template>

frontend/layers/base/app/components/BaseHeading.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
<script setup lang="ts">
2+
export type BaseHeadings = 'h2' | 'h3' | 'h4'
23
defineProps<{
3-
is:
4-
| 'h2'
5-
| 'h3',
4+
is: BaseHeadings,
65
size:
76
| 'lg'
8-
| 'md',
7+
| 'md'
8+
| 'xs',
99
}>()
1010
</script>
1111
1212
<template>
1313
<component
1414
:is
15-
class="font-semibold text-balance"
15+
class="font-bold text-balance"
1616
:class="[
1717
size === 'lg' && 'text-4xl tracking-[-.05rem]',
1818
size === 'md' && 'text-md',
19+
size === 'xs' && 'text-xl tracking-[-0.0063rem]',
1920
]"
2021
>
2122
<slot />

frontend/layers/base/nuxt.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export default defineNuxtConfig({
2727
icon: {
2828
mode: 'css',
2929
cssLayer: 'base',
30-
size: '1.25rem',
3130
localApiEndpoint: '/api/bff/_nuxt_icon',
3231
},
3332
/* eslint-enable perfectionist/sort-objects */
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script setup lang="ts"></script>
2+
3+
<template>
4+
<section
5+
class="flex flex-col gap-4xl justify-center items-center mt-11xl max-w-24xl mx-auto p-md"
6+
>
7+
<slot />
8+
</section>
9+
</template>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<script setup lang="ts">
2+
const { url } = useV1Login()
3+
4+
const videoRef = useTemplateRef('videoRef')
5+
6+
const playVideo = async () => {
7+
if (videoRef.value) {
8+
try {
9+
await videoRef.value.play()
10+
}
11+
catch {
12+
// Handle autoplay restrictions silently
13+
}
14+
}
15+
}
16+
17+
// Play animation when entering viewport to attract user attention
18+
const { stop: stopIntersectionObserver } = useIntersectionObserver(
19+
videoRef,
20+
([ entry ]) => {
21+
if (entry && entry.isIntersecting) {
22+
playVideo()
23+
}
24+
},
25+
{
26+
rootMargin: '0px 0px -50px 0px',
27+
threshold: 0.5,
28+
},
29+
)
30+
31+
const playOnInteraction = () => {
32+
playVideo()
33+
}
34+
35+
// Cleanup on unmount
36+
onUnmounted(() => {
37+
stopIntersectionObserver()
38+
})
39+
</script>
40+
41+
<template>
42+
<div class="w-full flex flex-col gap-4xl justify-center items-center">
43+
<BaseHeading
44+
is="h2"
45+
id="staking-hub"
46+
size="lg"
47+
class="text-center "
48+
>
49+
{{ $t('products.landing_page.staking_hub.title') }}
50+
</BaseHeading>
51+
<p>{{ $t('products.landing_page.staking_hub.description') }}</p>
52+
<BaseButton
53+
leading-icon="coins"
54+
trailing-icon="arrow-up-right"
55+
variant="branded"
56+
size="xl"
57+
:to="url"
58+
>
59+
{{ $t('products.landing_page.staking_hub.action.go_to_stakinghub') }}
60+
</BaseButton>
61+
<div
62+
class="grid grid-cols-1 md:grid-cols-3 md:flex-row gap-xl min-h-[var(--stakinghub-card-height)] lg:min-h-[var(--stakinghub-card-height-lg)]"
63+
style="--stakinghub-card-height: 31rem; --stakinghub-card-height-lg: 38.75rem;"
64+
>
65+
<BaseCard
66+
title-is="h3"
67+
title-icon="file-code"
68+
:title="$t('products.landing_page.staking_hub.cards.notifications.title')"
69+
class="min-h-[var(--stakinghub-card-height)]"
70+
>
71+
<p class="text-md font-semibold">
72+
{{ $t('products.landing_page.staking_hub.cards.notifications.subtitle') }}
73+
</p>
74+
<p class="text-sm">
75+
{{ $t('products.landing_page.staking_hub.cards.notifications.description') }}
76+
</p>
77+
<div class="flex-grow px-5xl flex items-center justify-center">
78+
<BaseIcon
79+
name="bell"
80+
class="size-[6.25rem]"
81+
/>
82+
</div>
83+
<template #footer>
84+
<BaseButton
85+
variant="branded"
86+
size="xl"
87+
full
88+
to="/notifications"
89+
>
90+
{{ $t('products.landing_page.staking_hub.cards.notifications.action') }}
91+
</BaseButton>
92+
</template>
93+
</BaseCard>
94+
<BaseCard
95+
title-is="h3"
96+
title-icon="file-code"
97+
:title="$t('products.landing_page.staking_hub.cards.staking_mobile_app.title')"
98+
class="min-h-[var(--stakinghub-card-height)] relative overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-300 [&>div:first-child]:relative [&>div:first-child]:z-10"
99+
tabindex="0"
100+
@mouseenter="playOnInteraction"
101+
@focus="playOnInteraction"
102+
>
103+
<video
104+
ref="videoRef"
105+
poster="/assets-2usdf/img/rotating-mobile-frame.webp"
106+
class="absolute top-[0] left-[0] w-full h-full object-cover z-0"
107+
muted
108+
playsinline
109+
preload="metadata"
110+
>
111+
<source
112+
src="/assets-2usdf/img/rotating-mobile.mp4"
113+
type="video/mp4"
114+
>
115+
</video>
116+
<!-- Dark overlay for text readability -->
117+
<div class="absolute top-[0] left-[0] w-full h-full bg-black/70 z-5" />
118+
<div class="relative z-10 p-6 text-white flex flex-col gap-2xl">
119+
<p class="text-md font-semibold">
120+
{{ $t('products.landing_page.staking_hub.cards.staking_mobile_app.subtitle') }}
121+
</p>
122+
<p>{{ $t('products.landing_page.staking_hub.cards.staking_mobile_app.description') }}</p>
123+
</div>
124+
<template #footer>
125+
<div class="flex gap-2xl justify-center items-start relative z-10">
126+
<NuxtLink
127+
to="https://apps.apple.com/app/beaconchain-dashboard/id1541822121"
128+
class="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-300"
129+
target="_blank"
130+
>
131+
<span class="sr-only">{{ $t('products.landing_page.staking_hub.cards.staking_mobile_app.action-download-appstore') }}</span>
132+
<img
133+
src="/assets-2usdf/img/app-store-btn.svg"
134+
class="w-auto h-[2.56rem]"
135+
>
136+
</NuxtLink>
137+
<NuxtLink
138+
to="https://play.google.com/store/apps/details?id=in.beaconcha.mobile"
139+
class="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-300"
140+
target="_blank"
141+
>
142+
<span class="sr-only">{{ $t('products.landing_page.staking_hub.cards.staking_mobile_app.action-download-playstore') }}</span>
143+
<img
144+
src="/assets-2usdf/img/play-store-btn.svg"
145+
class="w-auto h-[2.875rem]"
146+
>
147+
</NuxtLink>
148+
</div>
149+
</template>
150+
</BaseCard>
151+
<BaseCard
152+
title-is="h3"
153+
title-icon="file-code"
154+
:title="$t('products.landing_page.staking_hub.cards.validator_dashboards.title')"
155+
class="min-h-[var(--stakinghub-card-height)] bg-[url('/assets-2usdf/img/validator-bg.svg')] bg-[length:440px_440px] bg-[position:center_calc(100%+180px)] bg-no-repeat"
156+
>
157+
<p class="text-md font-semibold">
158+
{{ $t('products.landing_page.staking_hub.cards.validator_dashboards.subtitle') }}
159+
</p>
160+
<p>{{ $t('products.landing_page.staking_hub.cards.validator_dashboards.description') }}</p>
161+
<template #footer>
162+
<BaseButton
163+
variant="branded"
164+
size="xl"
165+
full
166+
to="/dashboard"
167+
>
168+
{{ $t('products.landing_page.staking_hub.cards.validator_dashboards.action') }}
169+
</BaseButton>
170+
</template>
171+
</BaseCard>
172+
</div>
173+
</div>
174+
</template>

0 commit comments

Comments
 (0)