first commit

This commit is contained in:
2024-03-20 00:12:01 +03:00
commit 1e0ab27b74
148 changed files with 12934 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
.reviewCard {
margin-top: 48px;
border-bottom-left-radius: 40px !important;
border-bottom-right-radius: 40px !important;
overflow: unset !important;
--on-background: #303309;
--on-background-secondary: #616825;
--background: #e6f45d;
}
.reviewCardContent {
margin: -36px;
margin-top: 36px !important;
}
.googlePlayButton {
width: 100%;
}
.main {
padding: 48px;
padding-top: 0;
display: flex;
flex-direction: column;
gap: 48px;
max-width: 1500px;
width: 100%;
align-self: center;
}
.cardsRow {
display: flex;
flex-direction: row;
gap: 48px;
}
.stores {
display: flex;
flex-direction: column;
gap: 16px;
}
.additionalStores {
display: flex;
flex-direction: row;
&>*:first-child {
flex-grow: 1;
}
}
@media screen and (max-width: 980px) {
.cardsRow {
flex-direction: column;
}
.reviewCard {
margin-top: 0;
}
}
@media screen and (max-width: 730px) {
.main {
padding: 24px;
gap: 24px;
}
.cardsRow {
gap: 24px;
}
}
@media screen and (max-width: 500px) {
.main {
padding: 16px;
gap: 16px;
}
.cardsRow {
gap: 16px;
}
.homeLink {
margin: 0 auto;
margin-top: 44px;
}
.description {
text-align: center;
}
}

View File

@@ -0,0 +1,216 @@
.header {
min-height: 30vh;
padding: 220px 78px;
padding-top: 330px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
position: relative;
width: 100%;
flex-shrink: 0;
}
.content {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
max-width: 1300px;
margin: 0 auto;
}
.description {
font-weight: 600;
font-size: 2rem;
max-width: 780px;
color: #4F3527;
line-height: 150%;
.appName {
font-weight: 700;
color: #361400;
}
.hobbyProject {
}
.notPlanEarnMoney {
margin: 0px 20px;
span {
box-shadow: -16px 0px 0px 2px #FF005C, 16px 0px 0px 2px #FF005C;
background-color: #FF005C;
color: #fff;
box-decoration-break: clone;
border-radius: 9px;
}
}
.contributeArrowWrapper {
display: inline-block;
}
.contributeArrow {
position: relative;
display: inline;
div {
position: absolute;
top: 13px;
left: 26px;
}
}
}
.chipsContainer {
flex-grow: 1;
align-items: center;
display: flex;
flex-direction: column;
transform: rotate(-25deg);
width: max-content;
}
.chip {
padding: 12px 32px;
padding-right: 22px;
border-radius: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
gap: 9px;
font-size: 1.62rem;
font-weight: 600;
width: fit-content;
.icon {
display: flex;
}
}
.openSourceChip {
background-color: #DDF9D0;
color: #339704;
transform: translateX(-50px);
svg {
color: #339704;
width: 35px;
height: 35px;
}
}
.madeWithLoveChip {
background-color: #FFEEF2;
color: #861118;
transform: translateX(50px);
svg {
color: #FF003D;
width: 35px;
height: 35px;
}
}
.gradientContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: calc(100% + 300px);
z-index: -100;
}
.transitionToBackgound {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 300px;
background: linear-gradient(180deg, #ffffff00 0%, #ffffff 300px);
}
@media (max-width: 1250px) {
.chipsContainer {
position: absolute;
right: 0;
left: -60px;
margin: auto;
top: 230px;
flex-direction: row;
transform: rotate(356deg);
z-index: -1;
}
.openSourceChip {
transform: translate(90px, -20px) rotate(-15deg);
}
.madeWithLoveChip {
transform: translateX(0px);
}
.header {
padding-top: 390px;
}
}
@media (max-width: 940px) {
.header {
padding: 200px 48px;
padding-top: 360px;
}
.description {
text-align: justify;
max-width: 100%;
}
}
@media (max-width: 800px) {
.header {
padding: 130px 48px;
padding-top: 360px;
}
.description {
font-size: 1.5rem;
line-height: 170%;
.contributeArrow {
div {
top: 8px;
}
svg {
width: auto;
height: 150px;
}
}
}
}
@media (max-width: 640px) {
.header {
padding: 110px 36px;
padding-top: 360px;
}
.chipsContainer {
top: 200px;
scale: 0.7;
}
.description {
.contributeArrow {
div {
top: 10px;
}
svg {
width: auto;
height: 130px;
}
}
}
}

View File

@@ -0,0 +1,127 @@
import styles from './header.module.scss';
import { TopBar } from '@/components/topBar';
import useLocale from '@/utils/useLocale';
import { LocalesMap } from '@/utils/useLocale';
import { clsx } from 'clsx';
import LoveIcon from '@/assets/icons/love.svg';
import OpenSourceIcon from '@/assets/icons/open-source.svg';
import { Gradients } from '@/components/gradients';
import ContributeArrow from '@/assets/images/contribute-arrow.svg';
import ContributeArrowInvert from '@/assets/images/contribute-arrow-invert.svg';
import { useEffect, useRef, useState } from 'react';
import useMediaQuery from '@/utils/useMediaQuery';
export const locales: LocalesMap = {
ru: {
appName: 'RideBus',
description: (
<>
<span className={styles.appName}>RideBus</span> — это <span className={styles.hobbyProject}>хобби-проект</span>{' '}
и я <span className={styles.notPlanEarnMoney}><span>не&nbsp;планирую на нём зарабатывать</span></span>,{' '}
если вы хотите помочь, вот способ, как это <span className={styles.contributeArrowWrapper}>сделать<Arrow /></span>
</>
),
openSource: 'Open source',
madeWithLove: 'Сделано с',
},
en: {
appName: 'RideBus',
description: (
<>
<span className={styles.appName}>RideBus</span> is <span className={styles.hobbyProject}>a hobby project</span>{' '}
and I <span className={styles.notPlanEarnMoney}><span>do&nbsp;not&nbsp;plan to earn money&nbsp;on&nbsp;it</span></span>,{' '}
if you want to help, here is a way how to do <span className={styles.contributeArrowWrapper}>it <Arrow /></span>
</>
),
openSource: 'Open source',
madeWithLove: 'Made with',
},
};
function Arrow() {
const rootRef = useRef<HTMLDivElement>(null);
const [isInvertArrow, setIsInvertArrow] = useState(false);
const breakpoint640 = useMediaQuery('(max-width: 640px)');
const breakpoint800 = useMediaQuery('(max-width: 800px)');
useEffect(() => {
const arrowWidth = breakpoint640 ? 260 : breakpoint800 ? 300 : 460;
const handleResize = () => {
if (rootRef.current) {
const { left } = rootRef.current.getBoundingClientRect();
if (window.innerWidth < left + arrowWidth) {
setIsInvertArrow(true);
} else {
setIsInvertArrow(false);
}
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [breakpoint640, breakpoint800]);
let left = isInvertArrow ? '-130px' : '26px';
if (breakpoint640) {
left = isInvertArrow ? '-95px' : '16px';
}
if (breakpoint800) {
left = isInvertArrow ? '-95px' : '16px';
}
return (
<span ref={rootRef} className={styles.contributeArrow}>
<span style={{ width: '140px', display: 'inline-block' }} />
<div style={{ left }}>
{isInvertArrow ? <ContributeArrowInvert /> : <ContributeArrow />}
</div>
</span>
)
}
type ChipProps = {
className?: string;
label: string;
icon: any;
};
function Chip(props: ChipProps) {
const { className, label, icon } = props;
return (
<div className={clsx(styles.chip, className)}>
<span>{label}</span>
<span className={styles.icon}>{icon}</span>
</div>
);
}
export function Header() {
const t = useLocale(locales);
return (
<header className={styles.header}>
<TopBar notHide />
<div className={styles.content}>
<p className={styles.description}>{t('description')}</p>
<div className={styles.chipsContainer}>
<Chip className={styles.openSourceChip} label={t('openSource')} icon={(<OpenSourceIcon />)} />
<Chip className={styles.madeWithLoveChip} label={t('madeWithLove')} icon={(<LoveIcon />)} />
</div>
</div>
<div className={styles.gradientContainer}>
<div
className={styles.transitionToBackgound}
/>
<Gradients />
</div>
</header>
);
}

View File

@@ -0,0 +1 @@
export * from './header';

View File

@@ -0,0 +1,21 @@
import { LocalesMap } from '@/utils/useLocale';
export const locales: LocalesMap = {
ru: {
pageTitle: 'RideBus - Вклад',
pageDescription:
'RideBus — Приложение, которое поможет вам удобно смотреть расписание общественного транспорта',
appName: 'RideBus',
reviewTitle: 'Оставьте отзыв',
reviewDescription:
'Буду благодарен, если вы оставите свой отзыв о приложении. Отзывы очень важны и помогают улучшать приложение.',
},
en: {
pageTitle: 'RideBus - Contribute',
pageDescription: 'RideBus — An app that helps you conveniently view public transportation schedules',
appName: 'RideBus',
reviewTitle: 'Leave a review',
reviewDescription:
'I would be grateful if you could leave your feedback on the application. Reviews are very important and help us improve application.',
},
};

View File

@@ -0,0 +1 @@
export * from './shareCard';

View File

@@ -0,0 +1,49 @@
.card {
--on-background: #59321c;
--on-background-secondary: #866655;
--background: #ffefe9;
position: relative;
}
.description {
padding: 8px;
margin: -8px;
margin-top: 0;
padding-top: 6px;
}
.heartContiner {
position: absolute;
bottom: -18%;
right: -17%;
z-index: 0;
}
.heartImage {
width: 420px;
height: auto;
}
@media screen and (max-width: 980px) {
.card {
height: min-content;
}
.heartContiner {
bottom: -32%;
right: -4%;
}
}
@media screen and (max-width: 820px) {
.heartContiner {
bottom: -42%;
right: -4%;
}
.heartImage {
width: 300px;
}
.content {
margin-top: 0;
}
}

View File

@@ -0,0 +1,60 @@
import styles from './shareCard.module.scss';
import { Card } from '@/components/card';
import clsx from 'clsx';
import useLocale, { LocalesMap } from '@/utils/useLocale';
import { CursorEffect } from '@/components/cursor';
const locales: LocalesMap = {
ru: {
appName: 'RideBus',
slogan: 'Приложение, которое поможет вам удобно смотреть расписание общественного транспорта',
title: 'Расскажите друзьям',
description:
'Мне очень важно, чтобы мои проекты приносили пользу. Если вы расскажете своим друзьям о своем опыте использования этого приложения и как оно помогло вам, я буду вам очень признателен.',
},
en: {
appName: 'RideBus',
slogan: 'An app that helps you conveniently view public transportation schedules',
title: 'Tell your friends',
description:
'It is very important to me that my projects bring benefits. If you tell your friends about your experience using this application and how it helped you, I will be very grateful to you.',
},
};
export function ShareCard() {
const t = useLocale(locales);
const ShareCard = (
<Card
className={clsx(styles.card)}
title={t('title')}
subtitle={t('description')}
backdropOnText
classes={{ subtitle: styles.description, content: styles.content }}
onClick={() => {
if (navigator.share) {
navigator
.share({
title: t('appName'),
text: t('slogan'),
url: 'https://ridebus.by/',
})
.then(() => {
console.log('Thanks for sharing!');
})
.catch(console.error);
}
}}
>
</Card>
);
if (typeof window !== 'undefined' && (navigator.share as any)) {
return (
<CursorEffect cursorPadding={8} cursorBorderRadius={36}>
{ShareCard}
</CursorEffect>
);
} else {
return ShareCard;
}
}

View File

@@ -0,0 +1,132 @@
.card {
--on-background: #122F60;
--on-background-secondary: #2C4678;
--background: #D8E2FF;
width: 100%;
height: 580px;
position: relative;
}
.mockupPhoneContainer {
position: absolute;
bottom: 0;
right: 0;
z-index: 0;
}
.backdropImage {
z-index: 1;
bottom: 0;
left: 0;
position: absolute;
width: auto;
height: 650px;
}
.ridebusImageMask {
z-index: 1;
position: absolute;
width: 640px;
height: 640px;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: 50%;
bottom: -140px;
right: -140px;
}
.ridebusImageAnchor {
bottom: 320px;
left: 320px;
position: absolute;
}
.ridebusImage {
z-index: 1;
bottom: -400px;
left: -400px;
position: absolute;
width: 800px;
height: 800px;
}
.screenshot {
z-index: 1;
bottom: 0;
left: 0;
position: absolute;
width: auto;
height: 650px;
}
.smallCitiesLine {
z-index: 1;
position: absolute;
display: flex;
flex-direction: row;
right: -50px;
bottom: 260px;
transform-origin: right center;
transform: rotate(4deg);
svg {
width: 220px;
height: auto;
}
}
.bigCitiesLine {
z-index: 1;
position: absolute;
display: flex;
flex-direction: row;
right: -280px;
bottom: 100px;
transform-origin: right center;
transform: rotate(356deg);
gap: 9px;
svg {
width: 260px;
height: auto;
}
}
@media (max-width: 1180px) {
.card {
height: 490px;
}
.ridebusImageMask {
width: 440px;
height: 440px;
bottom: -70px;
right: -110px;
}
.ridebusImageAnchor {
bottom: 220px;
left: 220px;
}
.smallCitiesLine {
bottom: 200px;
}
.bigCitiesLine {
bottom: 50px;
}
}
@media (max-width: 800px) {
.card {
height: 390px;
}
.smallCitiesLine {
bottom: 160px;
svg {
width: 170px;
}
}
.bigCitiesLine svg {
width: 200px;
}
}

View File

@@ -0,0 +1,144 @@
import { useRef } from 'react';
import styles from './citiesCard.module.scss';
import { Card } from '@/components/card';
import useLocale from '@/utils/useLocale';
import { LocalesMap } from '@/utils/useLocale';
import CityPolotskSmallEn from '@/assets/images/city-polotsk-small-en.svg';
import CityNovopolotskSmallEn from '@/assets/images/city-novopolotsk-small-en.svg';
import CityBigEn from '@/assets/images/city-big-en.svg';
import CityPolotskSmallRu from '@/assets/images/city-polotsk-small-ru.svg';
import CityNovopolotskSmallRu from '@/assets/images/city-novopolotsk-small-ru.svg';
import CityBigRu from '@/assets/images/city-big-ru.svg';
import maskUrl from '@/assets/images/cities-star-mask.svg?url';
import ridebusImage from '@/assets/images/cities-backdrop.jpg';
import Image from 'next/image';
import useFrame from '@/utils/useFrame';
import useScroll from '@/utils/useScroll';
import { useRouter } from 'next/router';
import useMediaQuery from '@/utils/useMediaQuery';
const locales: LocalesMap = {
ru: {
title: 'Поддержка нескольких городов',
description:
'Поддерживаются города Полоцк и Новополоцк, но мы планируем добавлять расписание общественного транспорта и для других городов Беларуси',
},
en: {
title: 'Multi-city support',
description:
'The cities of Polotsk and Novopolotsk are supported, but we plan to add public transportation schedules for other cities of Belarus as well',
},
};
function SmallCitiesLine() {
const router = useRouter();
const rootRef = useRef<HTMLDivElement>(null);
const breakpoint800 = useMediaQuery('(max-width: 800px)');
const cityWidth = breakpoint800 ? 170 : 220;
const swapWidth = cityWidth * 1;
useScroll((scrollY, delta, time) => {
const offset = -(scrollY / 6) - (time / 30);
if (rootRef.current) {
rootRef.current.style.transform = `rotate(3deg) translate3D(${offset % swapWidth + cityWidth}px, 0px, 0px)`;
}
}, {}, [cityWidth]);
const CityPolotsk = router.locale === 'ru' ? CityPolotskSmallRu : CityPolotskSmallEn;
return (
<div ref={rootRef} className={styles.smallCitiesLine}>
<CityPolotsk />
<CityPolotsk />
<CityPolotsk />
<CityPolotsk />
<CityPolotsk />
<CityPolotsk />
<CityPolotsk />
<CityPolotsk />
<CityPolotsk />
</div>
);
}
function BigCitiesLine() {
const router = useRouter();
const rootRef = useRef<HTMLDivElement>(null);
const breakpoint800 = useMediaQuery('(max-width: 800px)');
const cityWidth = (breakpoint800 ? 200 : 260) + 9;
const swapWidth = cityWidth * 1;
useScroll((scrollY, delta, time) => {
const offset = (scrollY / 6) + (time / 30);
if (rootRef.current) {
rootRef.current.style.transform = `rotate(356deg) translate3D(${offset % swapWidth}px, 0px, 0px)`;
}
}, {}, [cityWidth]);
const CityNovopolotsk = router.locale === 'ru' ? CityNovopolotskSmallRu : CityNovopolotskSmallEn;
return (
<div ref={rootRef} className={styles.bigCitiesLine}>
<CityNovopolotsk />
<CityNovopolotsk />
<CityNovopolotsk />
<CityNovopolotsk />
<CityNovopolotsk />
<CityNovopolotsk />
<CityNovopolotsk />
<CityNovopolotsk />
<CityNovopolotsk />
</div>
);
}
export function CitiesCard() {
const t = useLocale(locales);
const ridebusImageMaskRef = useRef<HTMLDivElement>(null);
const ridebusImageRef = useRef<HTMLDivElement>(null);
useFrame((delta, time) => {
if (ridebusImageMaskRef.current && ridebusImageRef.current) {
ridebusImageMaskRef.current.style.transform = `rotate(${-(time * 0.005) % 360}deg) translate3D(0px, 0px, 0px)`;
ridebusImageRef.current.style.transform = `rotate(${(time * 0.005) % 360}deg) translate3D(0px, 0px, 0px)`;
}
});
return (
<Card
className={styles.card}
title={t('title')}
subtitle={t('description')}
backdropOnText
classes={{ subtitle: styles.description, title: styles.title }}
>
<div className={styles.mockupPhoneContainer}>
<div
className={styles.ridebusImageMask}
ref={ridebusImageMaskRef}
style={{
maskImage: `url(${maskUrl.src})`,
}}>
<div ref={ridebusImageRef} className={styles.ridebusImageAnchor}>
<Image
className={styles.ridebusImage}
height={800}
width={800}
placeholder="blur"
alt=""
src={ridebusImage}
/>
</div>
</div>
<SmallCitiesLine />
<BigCitiesLine />
</div>
</Card>
);
}

View File

@@ -0,0 +1 @@
export * from './citiesCard';

View File

@@ -0,0 +1,54 @@
.cardWrapper {
flex-grow: 1;
}
.card {
--on-background: #2C4678;
--on-background-secondary: #455E91;
--background: #F3F3FA;
flex-grow: 1;
position: relative;
min-height: 240px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: none;
}
.title {
font-size: 2rem !important;
}
.cardContent {
flex-grow: 0 !important;
margin: 0 !important;
}
.circlesContainer {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}
.circlesBackground {
position: absolute;
width: 40%;
padding: 40%;
border-radius: 50%;
background-color: #D8E2FF;
}
.circlesFroreground {
position: absolute;
width: 25%;
padding: 25%;
border-radius: 50%;
background-color: #AEC6FF;
}

View File

@@ -0,0 +1,77 @@
import styles from './contributeCard.module.scss';
import { Card } from '@/components/card';
import useLocale from '@/utils/useLocale';
import { LocalesMap } from '@/utils/useLocale';
import Link from 'next/link';
import { animated, useSpring } from '@react-spring/web';
import { useRef } from 'react';
import { CursorEffect } from '@/components/cursor';
const locales: LocalesMap = {
ru: {
title: 'Вклад',
description: 'Узнайте, как вы можете помочь проекту',
},
en: {
title: 'Contribute',
description: 'Find out how you can help the project',
},
};
export function ContributeCard() {
const t = useLocale(locales);
const cardRef = useRef(null);
const [springsBackground, apiBackground] = useSpring(() => ({
scale: 1,
config: { mass: 4, tension: 350, friction: 30 },
}));
const [springsForeground, apiForeground] = useSpring(() => ({
scale: 1,
config: { mass: 2, tension: 350, friction: 20 },
}));
const handleEnter = () => {
apiForeground.start({
scale: 1.5,
});
apiBackground.start({
scale: 1.15,
});
};
const handleLeave = () => {
apiForeground.start({
scale: 1,
});
apiBackground.start({
scale: 1,
});
};
return (
<CursorEffect className={styles.cardWrapper} cursorBorderRadius={36} cursorPadding={12}>
<Link href="/contribute">
<Card
ref={cardRef}
className={styles.card}
title={t('title')}
subtitle={t('description')}
classes={{ content: styles.cardContent, title: styles.title }}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
>
<div className={styles.circlesContainer}>
<animated.div
className={styles.circlesBackground}
style={{ ...springsBackground }}
/>
<animated.div
className={styles.circlesFroreground}
style={{ ...springsForeground }}
/>
</div>
</Card>
</Link>
</CursorEffect>
);
}

View File

@@ -0,0 +1 @@
export * from './contributeCard';

View File

@@ -0,0 +1,116 @@
.root {
height: auto;
display: flex;
flex-direction: column;
width: 440px;
height: 580px;
flex-shrink: 0;
position: relative;
}
.freeForever, .wihthoutAds {
--on-background: #000000;
--background: #F3FF6C;
width: 100%;
position: relative;
border-radius: 100px;
z-index: -1;
flex-shrink: 0;
overflow: unset;
}
.freeForever {
padding: 48px 16px;
}
.wihthoutAds {
--on-background: #861118;
--background: #FFEEF2;
padding: 22px 16px;
}
.freeForeverTitle, .wihthoutAdsTitle {
left: 0;
right: 0;
text-align: center;
max-width: 100%;
}
.freeForeverTitleExclamationPoint {
position: relative;
svg {
position: absolute;
top: -46px;
left: 20px;
}
}
.wihthoutAdsTitleOutline {
position: relative;
svg {
position: absolute;
top: -46px;
left: -260px;
}
}
.imageCard {
background-color: #FFF0E8;
border-radius: 36px;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
z-index: 1;
}
.backdropImage, .screenshotImage, .shadowImage {
position: absolute;
top: 0px;
right: -110px;
width: 660px;
height: auto;
}
.screenshotImage {
z-index: 2;
}
.shadowImage {
z-index: 0;
}
@media (max-width: 1180px) {
.root {
height: 490px;
width: 360px;
}
.backdropImage, .screenshotImage, .shadowImage {
width: 540px;
right: -98px;
}
.freeForeverTitle, .wihthoutAdsTitle {
font-size: 1.5rem;
}
}
@media screen and (max-width: 980px) {
.root {
width: 100%;
z-index: 1;
}
.backdropImage, .screenshotImage, .shadowImage {
right: 0px;
}
}
@media screen and (max-width: 510px) {
.backdropImage, .screenshotImage, .shadowImage {
right: -105px;
}
}

View File

@@ -0,0 +1,78 @@
import styles from './freeCard.module.scss';
import clsx from 'clsx';
import { Card } from '@/components/card';
import useLocale from '@/utils/useLocale';
import { LocalesMap } from '@/utils/useLocale';
import Image from 'next/image';
import backdropImage from '@/assets/images/free-backdrop.png';
import screenshotEnImage from '@/assets/images/free-screenshot-en.png';
import screenshotRuImage from '@/assets/images/free-screenshot-ru.png';
import shadowImage from '@/assets/images/free-shadow.png';
import ExclamationPoint from '@/assets/images/free-exclamation-point.svg';
import Outline from '@/assets/images/free-outline.svg';
import { Fragment } from 'react';
import { useRouter } from 'next/router';
const locales: LocalesMap = {
ru: {
freeForever: 'Бесплатно навсегда',
wihthoutAds: 'Без рекламы',
},
en: {
freeForever: 'Free forever',
wihthoutAds: 'Without ads',
},
};
export function FreeCard() {
const router = useRouter();
const t = useLocale(locales);
return (
<div className={styles.root}>
<Image
className={clsx(styles.screenshotImage)}
width={695}
placeholder="blur"
alt=""
src={router.locale === 'ru' ? screenshotRuImage : screenshotEnImage}
/>
<div className={styles.imageCard}>
<Image
className={clsx(styles.backdropImage)}
width={695}
placeholder="blur"
alt=""
src={backdropImage}
/>
</div>
<Image
className={clsx(styles.shadowImage)}
width={695}
placeholder="blur"
alt=""
src={shadowImage}
/>
<Card
className={styles.freeForever}
title={
<Fragment>
{t('freeForever')}
<span className={styles.freeForeverTitleExclamationPoint}><ExclamationPoint /></span>
</Fragment>
}
classes={{ title: styles.freeForeverTitle }}
/>
<Card
className={styles.wihthoutAds}
title={
<Fragment>
{t('wihthoutAds')}
<span className={styles.wihthoutAdsTitleOutline}><Outline /></span>
</Fragment>
}
classes={{ title: styles.wihthoutAdsTitle }}
/>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './freeCard';

145
modules/home/header.tsx Normal file
View File

@@ -0,0 +1,145 @@
import styles from '@/modules/home/home.module.scss';
import useLocale from '@/utils/useLocale';
import { LocalesMap } from '@/utils/useLocale';
import clsx from 'clsx';
import { manrope, caveat } from '@/styles/fonts';
import ThinStarBig from '@/assets/images/thin-star-big.svg';
import ThickRoundStar5 from '@/assets/images/thick-round-star-5.svg';
import thickRoundStar6Url from '@/assets/images/thick-round-star-6.svg?url';
import ridebusFlat from '@/assets/images/ridebus-flat.jpg';
import Image from 'next/image';
import { Gradients } from '@/components/gradients';
import { useEffect, useRef, Fragment } from 'react';
import useScroll from '@/utils/useScroll';
import { TopBar } from '@/components/topBar';
import useFrame from '@/utils/useFrame';
import useMediaQuery from '@/utils/useMediaQuery';
const locales: LocalesMap = {
ru: {
slogan_line: (
<>
<span>Приложение,</span>
<span>которое</span>
<span>поможет</span>
<div>
<span>вам</span>
<div className={styles.thinStarBig}>
<ThinStarBig />
</div>
</div>
<span>с</span>
<span className={styles.spendWord}>лёгкостью</span>
<span>смотреть</span>
<span>расписание</span>
<span className={clsx(caveat.className, styles.wiselyWord)}>транспорта</span>
</>
),
},
en: {
slogan_line: (
<>
<span>Small</span>
<span>app</span>
<span>that</span>
<span>helps</span>
<div>
<span>you</span>
<div className={styles.thinStarBig}>
<ThinStarBig />
</div>
</div>
<span className={styles.spendWord}>conveniently</span>
<span>view</span>
<span className={clsx(caveat.className, styles.wiselyWord)}>
public
</span>
<span className={clsx(caveat.className, styles.wiselyWord)}>
transport
</span>
<span className={clsx(caveat.className, styles.wiselyWord)}>
schedules
</span>
</>
),
},
};
export default function Header() {
const t = useLocale(locales);
const sloganLineRef = useRef<HTMLSpanElement>(null);
const transitionToBackgoundRef = useRef<HTMLDivElement>(null);
const thickRoundStar6MaskRef = useRef<HTMLDivElement>(null);
const thickRoundStar6ImageRef = useRef<HTMLImageElement>(null);
const thickRoundStar5 = useRef<HTMLDivElement>(null);
const breakpoint600 = false;//useMediaQuery('(max-width: 600px)');
useScroll((scrollY, deltaTime, time) => {
const childs: HTMLSpanElement[] = [
...Array.from(sloganLineRef.current?.children || []),
] as HTMLSpanElement[];
childs.forEach((child, index) => {
const rawOffset = Math.min(-scrollY * 2 + index * 50, 0);
const offset = Math.max(-Math.pow(rawOffset / 50, 2), -500);
const rotateDirection = index % 2 === 0 ? 1 : -1;
child.style.transform = `translate3D(0px, ${offset}px, 0px) rotate(${(offset / 10) * rotateDirection}deg)`;
});
if (transitionToBackgoundRef.current && scrollY + 300 < window.innerHeight) {
transitionToBackgoundRef.current.style.height = `${scrollY + 300}px`;
}
if (thickRoundStar6MaskRef.current && scrollY - 300 < window.innerHeight) {
thickRoundStar6MaskRef.current.style.transform = `translate3D(0px, ${scrollY / 6}px, 0px) rotate(${scrollY / 8 + (time * 0.01) % 360}deg)`;
}
if (thickRoundStar6ImageRef.current && scrollY - 300 < window.innerHeight) {
thickRoundStar6ImageRef.current.style.transform = `rotate(${-scrollY / 8 - (time * 0.01) % 360}deg) translate3d(0,0,0)`;
}
if (thickRoundStar5.current && scrollY - 300 < window.innerHeight) {
thickRoundStar5.current.style.transform = `translate3D(-50%, -50%,0) rotate(${-scrollY / 6 - 10}deg)`;
}
}, { accelerator: 0.06 });
return (
<Fragment>
<header className={styles.header}>
<TopBar />
<div
className={styles.thickRoundStar6}
ref={thickRoundStar6MaskRef}
style={{ maskImage: `url(${thickRoundStar6Url.src})` }}
>
<Image
ref={thickRoundStar6ImageRef}
height={400}
width={400}
placeholder="blur"
alt=""
src={ridebusFlat}
/>
</div>
<h1 className={clsx(styles.slogan, manrope.className)}>
<span ref={sloganLineRef} className={styles.line1}>
{t('slogan_line')}
</span>
</h1>
<div className={styles.gradientContainer}>
<div
ref={transitionToBackgoundRef}
className={styles.transitionToBackgound}
/>
{!breakpoint600 && <Gradients />}
</div>
</header>
<div className={styles.thickRoundStar5}>
<div ref={thickRoundStar5}>
<ThickRoundStar5 />
</div>
</div>
</Fragment>
);
}

View File

@@ -0,0 +1,277 @@
.noiseOverlay {
position: absolute;
top: -500px;
left: -500px;
width: calc(100% + 1000px);
height: calc(100% + 1000px);
z-index: 10000;
opacity: 1;
mix-blend-mode: color-burn;
background-repeat: repeat;
background-size: 1024px;
pointer-events: none;
will-change: auto;
}
.header {
min-height: 100vh;
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0;
transform: translate3D(0, 0, 0);
}
.slogan {
max-width: 1300px;
width: 100%;
padding: 0px 50px;
text-align: justify;
margin-top: 8px;
font-size: 4rem;
font-weight: 600;
color: var(--on-background);
display: flex;
flex-direction: column;
gap: 56px;
.line1,
.line2 {
width: 100%;
display: flex;
flex-direction: row;
gap: 16px 34px;
justify-content: space-between;
flex-wrap: wrap;
span {
color: #000;
}
}
.thinStarBig {
position: relative;
color: #0058FF;
z-index: -1;
vertical-align: super;
height: 0;
display: inline-block;
transform: translateY(-12px) translateX(-3px);
svg {
position: absolute;
transform: translateX(-50%) translateY(-50%);
}
}
.spendWord {
font-weight: 600;
color: #1d4fac !important;
}
.wiselyWord {
font-size: 6rem;
line-height: 80%;
font-weight: 600;
color: #715573 !important;
padding-right: 20px;
margin-right: -20px;
}
}
.header p {
max-width: 530px;
text-align: center;
margin-top: 8px;
font-size: 1.14rem;
font-weight: 500;
color: var(--on-background-secondary);
}
.thickRoundStar6 {
position: absolute;
top: 20vh;
left: -125px;
z-index: 0;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: 50%;
width: 250px;
height: 250px;
display: flex;
align-items: center;
justify-content: center;
img {
position: absolute;
width: 400px;
height: 400px;
object-fit: contain;
}
}
.thickRoundStar5 {
position: relative;
max-width: 1500px;
width: 100%;
margin: auto;
z-index: 100000;
div {
position: absolute;
top: -50px;
right: 50px;
transform: translate(-50%, -50%) rotate(-10deg);
color: #9f7ea1;
}
}
.gradientContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: calc(100% + 300px);
z-index: -100;
}
.transitionToBackgound {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 300px;
background: linear-gradient(180deg, #ffffff00 0%, #ffffff 300px);
transform: translate3D(0, 0, 0);
}
.backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: -1000;
}
.main {
padding: 48px;
padding-top: 0;
display: flex;
flex-direction: column;
gap: 48px;
max-width: 1500px;
width: 100%;
align-self: center;
}
.cardsRow {
display: flex;
flex-direction: row;
gap: 48px;
}
.card {
flex-basis: 50%;
}
.storesBlock {
display: flex;
flex-direction: column;
gap: 12px;
}
@media screen and (max-width: 1240px) {
.main {
padding: 36px;
gap: 36px;
}
.cardsRow {
gap: 36px;
}
}
@media screen and (max-width: 980px) {
.slogan {
font-size: 3rem;
gap: 16px;
.wiselyWord {
font-size: 4.6rem;
}
}
.cardsRow {
flex-direction: column;
}
.howItsWorkCard {
height: 550px;
padding: 32px;
}
.mockupPhoneContainer {
top: 25%;
}
.mockupPhone {
width: 700px;
height: 600px;
}
.screenshotMain {
width: 176px;
}
}
@media screen and (max-width: 800px) {
.main {
padding: 24px;
gap: 24px;
}
.cardsRow {
gap: 24px;
}
}
@media screen and (max-width: 730px) {
.main {
padding: 24px;
gap: 24px;
}
.cardsRow {
gap: 24px;
}
}
@media screen and (max-width: 640px) {
.thickRoundStar6 {
width: 125px;
height: 125px;
left: -62px;
top: 8vh;
}
}
@media screen and (max-width: 500px) {
.main {
padding: 16px;
gap: 16px;
}
.slogan {
font-size: 2rem;
padding: 0px 12px;
gap: 16px;
}
.wiselyWord {
font-size: 3rem !important;
}
.cardsRow {
gap: 16px;
}
}

View File

@@ -0,0 +1,82 @@
.card {
--on-background: #00276e;
--on-background-secondary: #376ed3;
--background: #dbe8ff;
width: 100%;
height: 650px;
position: relative;
}
.mockupPhoneContainer {
position: absolute;
top: 4%;
right: 5%;
z-index: 0;
}
.mockupPhone {
z-index: 1;
position: relative;
width: 939px;
height: auto;
}
.screenshotMain {
position: absolute;
top: 20.1%;
right: 27.92%;
transform: rotate(6.54deg);
width: 266.9px;
height: auto;
z-index: 1;
}
.title {
font-size: 3rem !important;
}
.description {
padding: 8px;
margin: -8px;
margin-top: 0;
padding-top: 6px;
}
@media (max-width: 1180px) {
.card {
height: 560px;
}
.title {
font-size: 2rem !important;
}
}
@media screen and (max-width: 800px) {
.card {
height: 440px;
padding: 32px;
}
.mockupPhoneContainer {
top: 16%;
right: -14%;
}
.mockupPhone {
width: 723.338px;
height: 508.2px;
}
.screenshotMain {
width: 206.1px;
right: 27.9%;
top: 20%;
}
}
@media screen and (max-width: 480px) {
.mockupPhoneContainer {
right: -32%;
}
}

View File

@@ -0,0 +1,80 @@
import Image from 'next/image';
import phoneMockupLightImage from '@/assets/images/phone-mockup-light.png';
import screenshotEnImage from '@/assets/images/screenshot-en.png';
import screenshotRuImage from '@/assets/images/screenshot-ru.png';
import styles from './howItsWorkCard.module.css';
import { Card } from '@/components/card';
import useLocale from '@/utils/useLocale';
import { LocalesMap } from '@/utils/useLocale';
import { useRouter } from 'next/router';
import clsx from 'clsx';
import { useRef } from 'react';
import useScroll from '@/utils/useScroll';
const locales: LocalesMap = {
ru: {
title: 'Как это работает?',
description:
'Записывая каждую трату, вы отрезвляете разум и даете представление о том, сколько и как вы можете потратить.',
},
en: {
title: 'How its work?',
description:
'Writing down each spending sobers the mind and gives you an idea of how much and how you can spend',
},
};
export function HowItsWorkCard() {
const router = useRouter();
const t = useLocale(locales);
const ref = useRef<any>();
const imageRef = useRef<any>();
useScroll((scrollY) => {
if (!imageRef.current || !ref.current) return;
const rootNode = ref.current;
const imageNode = imageRef.current;
const scrollOffsetCard = -Math.min(scrollY - rootNode.offsetTop + window.innerHeight - 300, 0);
rootNode.style.transform = `translate3D(0px, ${Math.expm1(scrollOffsetCard / 60)}px, 0px) rotate(${-Math.expm1(scrollOffsetCard / 120)}deg)`;
rootNode.style.transformOrigin = 'right top';
const scrollOffset = rootNode.offsetTop - scrollY;
imageNode.style.transform = `translate3D(0px, ${-(scrollOffset / 16)}px, 0px)`;
}, {
accelerator: 0.5,
});
return (
<Card
ref={ref}
className={styles.card}
title={t('title')}
subtitle={t('description')}
backdropOnText
classes={{ subtitle: styles.description, title: styles.title }}
>
<div ref={imageRef} className={styles.mockupPhoneContainer}>
<Image
className={clsx(styles.mockupPhone)}
height={1200}
width={1708}
placeholder="blur"
alt=""
src={phoneMockupLightImage}
/>
<Image
className={styles.screenshotMain}
height={2340}
width={1080}
placeholder="blur"
alt=""
src={router.locale !== 'ru' ? screenshotEnImage : screenshotRuImage}
/>
</div>
</Card>
);
}

View File

@@ -0,0 +1 @@
export * from './howItsWorkCard';

13
modules/home/locales.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { LocalesMap } from '@/utils/useLocale';
export const locales: LocalesMap = {
ru: {
pageTitle: 'RideBus',
pageDescription:
'RideBus — Приложение, которое поможет вам удобно смотреть расписание общественного транспорта',
},
en: {
pageTitle: 'RideBus',
pageDescription: 'RideBus — An app that helps you conveniently view public transportation schedules',
},
};