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,131 @@
.card {
border-radius: 36px;
overflow: hidden;
padding: 36px;
background-color: var(--background);
display: flex;
flex-direction: column;
}
.textWrapper {
position: relative;
z-index: 1;
}
.title {
color: var(--on-background);
font-size: 1.8rem;
max-width: 500px;
font-weight: 800;
z-index: 1;
}
.subtitle {
color: var(--on-background-secondary);
font-size: 1.125rem;
font-weight: 600;
max-width: 500px;
margin-top: 6px;
z-index: 1;
}
.backdropOnText {
background-color: var(--background);
padding: 0.5rem 8px;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.backdropOnText {
background-color: var(--background);
padding: 0 8px;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
z-index: 1 !important;
position: relative;
box-shadow: 0px 5px 0px var(--background);
}
.backdropTextHelper {
position: absolute;
top: 0;
z-index: 0 !important;
pointer-events: none;
user-select: none;
}
.backdropTextHelper .backdropOnText {
box-shadow: 00px -5px 0px var(--background);
}
.content {
display: flex;
flex-direction: column;
flex-grow: 1;
margin-top: 48px;
justify-content: flex-end;
}
.cardLinkWrapper {
margin-top: 16px;
width: 100%;
z-index: 1;
position: relative;
max-width: 360px;
}
.cardLink {
border-radius: 16px;
background-color: var(--background);
a {
cursor: none !important;
}
}
.cardLink:first-child {
margin-top: auto;
}
.cardLinkContent {
padding: 16px;
padding-right: 48px;
}
.icon {
position: absolute;
top: 16px;
right: 16px;
color: var(--on-background);
}
.cardLink .title {
color: var(--on-background);
font-size: 1.2rem;
font-weight: 600;
line-height: 100%;
}
.cardLink .subtitle {
color: var(--on-background-secondary);
font-size: 0.75rem;
margin-top: 4px;
}
@media screen and (max-width: 800px) {
.title {
font-size: 1.5rem;
}
.subtitle {
font-size: 0.85rem;
}
}
@media screen and (max-width: 620px) {
.cardLink {
width: 100%;
max-width: unset;
}
}

84
components/card/card.tsx Normal file
View File

@@ -0,0 +1,84 @@
import { clsx } from 'clsx';
import styles from './card.module.scss';
import { manrope } from '@/styles/fonts';
import React, { forwardRef } from 'react';
type CardProps = Omit<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
'title'
> & {
title?: React.ReactNode;
subtitle?: React.ReactNode;
backdropOnText?: boolean;
classes?: { [key: string]: string };
};
const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
props: CardProps,
ref
) {
const {
title,
subtitle,
backdropOnText,
children,
classes = {},
className: restClassName,
...restProps
} = props;
return (
<div ref={ref} className={clsx(styles.card, restClassName)} {...restProps}>
<div className={clsx(styles.textWrapper, classes.textWrapper)}>
<h3
className={clsx(manrope.className, styles.title, classes.title)}
>
<span className={clsx(backdropOnText && styles.backdropOnText)}>
{title}
</span>
</h3>
{backdropOnText && (
<h3
className={clsx(
manrope.className,
styles.title,
classes.title,
styles.backdropTextHelper
)}
>
<span className={clsx(backdropOnText && styles.backdropOnText)}>
{title}
</span>
</h3>
)}
</div>
{subtitle && (
<div className={clsx(styles.textWrapper, classes.textWrapper)}>
<p className={clsx(styles.subtitle, classes.subtitle)}>
<span className={clsx(backdropOnText && styles.backdropOnText)}>
{subtitle}
</span>
</p>
{backdropOnText && (
<p
className={clsx(
styles.subtitle,
classes.subtitle,
styles.backdropTextHelper
)}
>
<span className={clsx(backdropOnText && styles.backdropOnText)}>
{subtitle}
</span>
</p>
)}
</div>
)}
{children && (
<div className={clsx(styles.content, classes.content)}>{children}</div>
)}
</div>
);
});
export { Card };

View File

@@ -0,0 +1,64 @@
import { clsx } from 'clsx';
import Link from 'next/link';
import styles from './card.module.scss';
import LinkIcon from '@/assets/icons/link.svg';
import { animated, useSpring } from '@react-spring/web';
import { CursorEffect } from '@/components/cursor';
type CardProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
title?: string;
hrefTitle?: string;
href: string;
};
export function CardLink(props: CardProps) {
const {
title,
hrefTitle,
href,
className: restClassName,
...restProps
} = props;
const [springs, api] = useSpring(() => ({
scale: 1,
config: { mass: 1, tension: 150, friction: 10 },
}));
const handleEnter = () => {
api.start({
scale: 1.01,
});
};
const handleLeave = () => {
api.start({
scale: 1,
});
};
return (
<CursorEffect
className={clsx(styles.cardLinkWrapper, restClassName)}
cursorBorderRadius={16}
cursorPadding={6}
>
<animated.div
className={clsx(styles.cardLink)}
style={{ ...springs }}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
>
<Link href={href} target="_blank">
<div className={styles.cardLinkContent}>
<h3 className={styles.title}>{title}</h3>
<p className={styles.subtitle}>{hrefTitle}</p>
</div>
<LinkIcon className={styles.icon} />
</Link>
</animated.div>
</CursorEffect>
);
}

View File

@@ -0,0 +1,2 @@
export * from './card';
export * from './cardLink';

View File

@@ -0,0 +1,29 @@
.root {
position: fixed;
top: 0;
left: 0;
z-index: 1000000;
pointer-events: none;
overflow: hidden;
}
.hovered {
backdrop-filter: invert(1);
}
.idle {
background-color: #000;
.spot {
opacity: 0;
}
}
.spot {
position: absolute;
top: 0;
left: 0;
border-radius: 50%;
background-image: radial-gradient(#ffffffe6 0%, #ffffffbd 30%, #ffffff6e 50%, #ffffff00 70%);
opacity: 0.2;
}

View File

@@ -0,0 +1,293 @@
import useFrame from '@/utils/useFrame';
import { Fragment, useEffect, useRef } from 'react';
import styles from './cursor.module.css';
import { smooth } from '@/utils/smooth';
type CursorProps = {
children: React.ReactNode;
}
export function Cursor(props: CursorProps) {
const { children } = props;
const mousePosition = useRef<{
x: number;
y: number;
cursorPadding: number;
cursorBorderRadius: number;
targetRect: DOMRect | null;
target: { link: boolean; button: boolean; cursorEffector: boolean };
pressed: boolean;
active: boolean;
}>({
x: 0,
y: 0,
cursorPadding: 0,
cursorBorderRadius: 0,
targetRect: null,
target: { link: false, button: false, cursorEffector: false },
pressed: false,
active: false,
});
const cursorRef = useRef<HTMLDivElement>(null);
const cursorSpotRef = useRef<HTMLDivElement>(null);
const prevCursorState = useRef<{
x: number;
y: number;
width: number;
height: number;
borderRadius: number;
}>({
x: 0,
y: 0,
width: 0,
height: 0,
borderRadius: 0,
});
useEffect(() => {
let isReadyForHide = false;
let isTouch = false;
const updateTargetFromPath = (path: any[]) => {
const isLink = path.find((el) => el?.tagName === 'A');
const isButton = path.find((el) => el?.tagName === 'BUTTON');
const isCursorEffector = path.find((el) =>
el?.getAttribute?.('data-cursor-effect')
);
const cursorPadding = +(
path
.find((el) => el?.getAttribute?.('data-cursor-padding') !== null)
?.getAttribute?.('data-cursor-padding') || 16
);
const cursorBorderRadius = +(
path
.find(
(el) => el?.getAttribute?.('data-cursor-border-radius') !== null
)
?.getAttribute?.('data-cursor-border-radius') || 0
);
let targetRect = null;
if (isLink) {
targetRect = isLink.getBoundingClientRect();
}
if (isButton) {
targetRect = isButton.getBoundingClientRect();
}
if (isCursorEffector) {
targetRect = isCursorEffector.getBoundingClientRect();
}
mousePosition.current = {
...mousePosition.current,
cursorPadding,
cursorBorderRadius,
targetRect,
target: {
link: Boolean(isLink),
button: Boolean(isButton),
cursorEffector: Boolean(isCursorEffector),
},
};
};
const handleMouseMove = (event: MouseEvent) => {
if (isTouch) return;
updateTargetFromPath(event.composedPath());
if (!mousePosition.current.active) {
prevCursorState.current = {
...prevCursorState.current,
x: event.clientX,
y: event.clientY,
};
}
isReadyForHide = false;
mousePosition.current = {
...mousePosition.current,
x: event.clientX,
y: event.clientY,
active: true,
};
};
const handleMouseLeave = () => {
isReadyForHide = true;
setTimeout(() => {
if (!isReadyForHide) return;
console.log('mouse leave');
mousePosition.current = {
...mousePosition.current,
active: false,
};
}, 300);
};
const handleScroll = () => {
if (isTouch) return;
updateTargetFromPath(
document.elementsFromPoint(
mousePosition.current.x,
mousePosition.current.y
)
);
};
const handleMouseDown = (event: MouseEvent) => {
if (isTouch) return;
updateTargetFromPath(event.composedPath());
mousePosition.current = {
...mousePosition.current,
pressed: true,
};
};
const handleMouseUp = (event: MouseEvent) => {
if (isTouch) return;
updateTargetFromPath(event.composedPath());
mousePosition.current = {
...mousePosition.current,
pressed: false,
};
};
const handlePointerDown = (event: PointerEvent) => {
isTouch = event.pointerType === 'touch';
if (isTouch) handleMouseLeave();
};
window.addEventListener('pointerdown', handlePointerDown);
window.addEventListener('mousemove', handleMouseMove);
window.document.body.addEventListener('mouseleave', handleMouseLeave);
window.addEventListener('scroll', handleScroll);
window.addEventListener('mousedown', handleMouseDown);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('pointerdown', handlePointerDown);
window.removeEventListener('mousemove', handleMouseMove);
window.document.body.removeEventListener('mouseleave', handleMouseLeave);
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('click', handleMouseDown);
window.removeEventListener('mouseup', handleMouseUp);
};
}, []);
useFrame((delta) => {
let x = mousePosition.current.x;
let y = mousePosition.current.y;
let width = 30;
let height = 30;
let borderRadius = 0;
let gapHover = mousePosition.current.cursorPadding;
const targetRect = mousePosition.current.targetRect;
const isHovered =
(mousePosition.current.target.link ||
mousePosition.current.target.button ||
mousePosition.current.target.cursorEffector) &&
targetRect !== null;
if (!mousePosition.current.active) {
width = 0;
height = 0;
borderRadius = 15;
} else if (isHovered) {
const targetCenterX = targetRect.left + targetRect.width / 2;
const targetCenterY = targetRect.top + targetRect.height / 2;
width = targetRect.width + gapHover * 2;
height = targetRect.height + gapHover * 2;
x = x * 0.05 + targetCenterX * 0.95 - width / 2;
y = y * 0.05 + targetCenterY * 0.95 - height / 2;
} else {
width = 30;
height = 30;
x = x - width / 2;
y = y - height / 2;
borderRadius = 15;
}
if (mousePosition.current.pressed) {
width -= 10;
height -= 10;
x += 5;
y += 5;
gapHover -= 5;
}
if (mousePosition.current.cursorBorderRadius) {
borderRadius = mousePosition.current.cursorBorderRadius + gapHover;
} else {
borderRadius = Math.min(width / 2, height / 2);
}
x = smooth(prevCursorState.current.x, x, 0.02 * delta);
y = smooth(prevCursorState.current.y, y, 0.02 * delta);
width = smooth(prevCursorState.current.width, width, 0.02 * delta);
height = smooth(prevCursorState.current.height, height, 0.02 * delta);
borderRadius = smooth(
prevCursorState.current.borderRadius,
borderRadius,
0.02 * delta
);
prevCursorState.current = {
...prevCursorState.current,
x,
y,
width,
height,
borderRadius,
};
const cursorNode = cursorRef.current;
const cursorSpotNode = cursorSpotRef.current;
if (cursorNode !== null && cursorSpotNode !== null) {
cursorNode.style.transform = `translate3D(${x}px, ${y}px, 0)`;
cursorNode.style.width = `${width}px`;
cursorNode.style.height = `${height}px`;
cursorNode.style.borderRadius = `${Math.min(borderRadius, 36 + gapHover)}px`;
const size = Math.max((width + height) / 2, 48);
cursorSpotNode.style.transform = `translate(${mousePosition.current.x - x - size / 2}px, ${mousePosition.current.y - y - size / 2}px)`;
cursorSpotNode.style.width = `${size}px`;
cursorSpotNode.style.height = `${size}px`;
if (isHovered) {
if (!cursorNode.classList.contains(styles.hovered)) {
cursorNode.classList.add(styles.hovered);
cursorNode.classList.remove(styles.idle);
}
} else {
if (!cursorNode.classList.contains(styles.idle)) {
cursorNode.classList.add(styles.idle);
cursorNode.classList.remove(styles.hovered);
}
}
}
});
return (
<Fragment>
<div ref={cursorRef} className={styles.root}>
<div ref={cursorSpotRef} className={styles.spot} />
</div>
{children}
</Fragment>
);
}

View File

@@ -0,0 +1,196 @@
import { smooth } from '@/utils/smooth';
import useFrame from '@/utils/useFrame';
import { useEffect, useRef } from 'react';
type CursorEffectProps = {
children: React.ReactNode;
className?: string;
effectDistance?: number;
effectForce?: number;
cursorPadding?: number;
cursorBorderRadius?: number;
};
const clamp = (value: number, min: number, max: number) => {
const minOffset = Math.max(value - min, 0);
return Math.min(minOffset / (max - min), 1);
};
const inRect = (x: number, y: number, rect: DOMRect, gap: number = 0) => {
return (
rect.left - gap < x &&
x < rect.right + gap &&
rect.top - gap < y &&
y < rect.bottom + gap
);
};
export function CursorEffect(props: CursorEffectProps) {
const {
children,
className,
effectDistance = 128,
effectForce = 6,
cursorPadding = 16,
cursorBorderRadius = 0,
} = props;
const cursorAffectorRef = useRef<HTMLDivElement>(null);
const cursorAffectoContainerrRef = useRef<HTMLDivElement>(null);
const mousePosition = useRef<{
x: number;
y: number;
targetRect: DOMRect | null;
}>({
x: 0,
y: 0,
targetRect: null,
});
const prevCursorState = useRef<{
x: number;
y: number;
}>({
x: 0,
y: 0,
});
useEffect(() => {
let isTouch = false;
const handleMouseMove = (event: MouseEvent) => {
if (isTouch) return;
let targetRect =
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
mousePosition.current = {
x: event.clientX,
y: event.clientY,
targetRect,
};
};
const handleScroll = () => {
if (isTouch) return;
let targetRect =
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
mousePosition.current = {
...mousePosition.current,
targetRect,
};
};
const handlePointerDown = (event: PointerEvent) => {
isTouch = event.pointerType === 'touch';
let targetRect =
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
mousePosition.current = {
...mousePosition.current,
targetRect,
x: -9999999,
y: -9999999,
};
};
window.addEventListener('pointerdown', handlePointerDown);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('pointerdown', handlePointerDown);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('scroll', handleScroll);
};
}, []);
useFrame((delta) => {
let x = 0;
let y = 0;
const gap = effectDistance;
const offsetMax = effectForce;
const mousePos = mousePosition.current;
const rect = mousePosition.current.targetRect;
const isActive = rect && inRect(mousePos.x, mousePos.y, rect, gap);
if (isActive) {
const halfWidth = rect.width / 2;
const targetCenterX = rect.left + halfWidth;
const leftStart = rect.left - gap;
const leftMiddle = rect.left - offsetMax;
const rightMiddle = rect.right + offsetMax;
const rightEnd = rect.right + gap;
const affectX =
clamp(mousePos.y, rect.top - gap, rect.top) *
(1 - clamp(mousePos.y, rect.bottom, rect.bottom + gap));
if (mousePos.x < leftMiddle) {
x = -clamp(mousePos.x, leftStart, leftMiddle);
} else if (mousePos.x < targetCenterX) {
x = -(1 - clamp(mousePos.x, leftMiddle, targetCenterX));
} else if (mousePos.x < rightMiddle) {
x = clamp(mousePos.x, targetCenterX, rightMiddle);
} else if (mousePos.x < rightEnd) {
x = 1 - clamp(mousePos.x, rightMiddle, rightEnd);
}
x = x * affectX;
const halfHeight = rect.height / 2;
const targetCenterY = rect.top + halfHeight;
const topStart = rect.top - gap;
const topMiddle = rect.top - offsetMax;
const bottomMiddle = rect.bottom + offsetMax;
const bottomEnd = rect.bottom + gap;
const affectY =
clamp(mousePos.x, rect.left - gap, rect.left) *
(1 - clamp(mousePos.x, rect.right, rect.right + gap));
if (mousePos.y < topMiddle) {
y = -clamp(mousePos.y, topStart, topMiddle);
} else if (mousePos.y < targetCenterY) {
y = -(1 - clamp(mousePos.y, topMiddle, targetCenterY));
} else if (mousePos.y < bottomMiddle) {
y = clamp(mousePos.y, targetCenterY, bottomMiddle);
} else if (mousePos.y < bottomEnd) {
y = 1 - clamp(mousePos.y, bottomMiddle, bottomEnd);
}
y = y * affectY;
x = x * offsetMax;
y = y * offsetMax;
}
x = smooth(prevCursorState.current.x, x, 0.02 * delta);
y = smooth(prevCursorState.current.y, y, 0.02 * delta);
prevCursorState.current = { x, y };
if (cursorAffectorRef.current) {
cursorAffectorRef.current.style.transform = `translate3D(${x}px, ${y}px, 0)`;
}
});
return (
<div ref={cursorAffectoContainerrRef} className={className}>
<div
ref={cursorAffectorRef}
style={{ width: '100%', height: '100%' }}
data-cursor-effect
data-cursor-padding={cursorPadding}
data-cursor-border-radius={cursorBorderRadius}
>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,2 @@
export * from './cursor';
export * from './cursorEffect';

View File

@@ -0,0 +1,51 @@
.footer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 0px 48px 48px;
max-width: 1500px;
margin: auto;
}
.fatLittleStar {
position: relative;
margin-right: auto;
transform: translate(30px, 10px);
color: #0059ff;
z-index: -1;
svg {
position: absolute;
top: 0;
left: 0;
transform: translate(-50%, -50%);
}
}
.author {
font-weight: 700;
}
.xtimms {
color: #0059ff;
cursor: none !important;
}
@media screen and (max-width: 1240px) {
.footer {
padding: 0px 36px 36px;
}
}
@media screen and (max-width: 800px) {
.footer {
padding: 0px 24px 24px;
}
}
@media screen and (max-width: 500px) {
.footer {
padding: 0px 16px 16px;
}
}

View File

@@ -0,0 +1,63 @@
import useLocale, { LocalesMap } from '@/utils/useLocale';
import { clsx } from 'clsx';
import Link from 'next/link';
import styles from './footer.module.scss';
import FatLittleStar from '@/assets/images/fat-little-star.svg';
import { useRef } from 'react';
import useScroll from '@/utils/useScroll';
type CardProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
const localesMap: LocalesMap = {
ru: {
madeBy: 'сделано',
},
en: {
madeBy: 'made by',
},
};
export function Footer(props: CardProps) {
const { className: restClassName, ...restProps } = props;
const t = useLocale(localesMap);
const fatLittleStarRef = useRef<HTMLDivElement>(null);
useScroll((scrollY) => {
if (
typeof window === 'undefined' ||
window.document.scrollingElement === null
)
return;
const scrollOffset =
window.document.scrollingElement.scrollHeight -
window.innerHeight -
scrollY;
if (fatLittleStarRef.current) {
fatLittleStarRef.current.style.transform = `translate3D(30px, ${scrollOffset / -2 + 10}px, 0) rotate(${scrollOffset / 10}deg)`;
}
});
return (
<footer className={clsx(restClassName, styles.footer)} {...restProps}>
<div ref={fatLittleStarRef} className={styles.fatLittleStar}>
<FatLittleStar />
</div>
<p className={styles.author}>
{`${t('madeBy')} `}
<Link
className={styles.xtimms}
href="https://xtimms.t.me"
target="_blank"
prefetch={false}
>
@xtimms
</Link>
</p>
</footer>
);
}

View File

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

View File

@@ -0,0 +1,38 @@
.card {
border-radius: 36px;
overflow: hidden;
background-color: #a4a4a41c;
backdrop-filter: blur(18px);
a {
cursor: none;
}
}
.content {
display: flex;
flex-direction: row;
padding: 14px;
padding-left: 28px;
padding-right: 22px;
transition: 0.3s ease;
}
.linkIcon {
width: 42px;
height: 42px;
margin-left: auto;
}
.storeNameText {
color: #242424;
font-weight: 700;
font-size: 2rem;
margin-right: 8px;
}
@media screen and (max-width: 620px) {
.content {
padding: 36px;
}
}

View File

@@ -0,0 +1,28 @@
import styles from './githubCardButton.module.scss';
import Link, { LinkProps } from 'next/link';
import LinkIcon from '@/assets/icons/link.svg';
import clsx from 'clsx';
import { CursorEffect } from '@/components/cursor';
type GithubCardButtonProps = Omit<LinkProps, 'href'> & { className?: string };
export function GithubCardButton(props: GithubCardButtonProps) {
const { className: restCalssName, ...restProps } = props;
return (
<CursorEffect cursorBorderRadius={36} cursorPadding={12}>
<div className={clsx(styles.card, restCalssName)}>
<Link
{...restProps}
target="_blank"
href="https://github.com/ztimms73/ridebus"
>
<div className={styles.content}>
<span className={styles.storeNameText}>GitHub</span>
<LinkIcon className={styles.linkIcon} />
</div>
</Link>
</div>
</CursorEffect>
);
}

View File

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

View File

@@ -0,0 +1,14 @@
.linkWrapper {
display: inline-flex;
height: min-content;
}
.link {
display: inline-flex;
padding: 18px;
cursor: none;
}
.linkIcon {
color: var(--on-background);
}

View File

@@ -0,0 +1,30 @@
import styles from './githubLink.module.scss';
import Link, { LinkProps } from 'next/link';
import GithubIcon from '@/assets/icons/github.svg';
import clsx from 'clsx';
import { CursorEffect } from '@/components/cursor';
type GithubLinkProps = Omit<LinkProps, 'href'> & { className?: string };
export function GithubLink(props: GithubLinkProps) {
const { className: restClassName, ...restProps } = props;
return (
<CursorEffect
effectDistance={48}
effectForce={4}
cursorPadding={0}
className={clsx(styles.linkWrapper)}
>
<Link
{...restProps}
className={clsx(styles.link, restClassName)}
target="_blank"
href="https://github.com/ztimms73/ridebus"
prefetch={false}
>
<GithubIcon className={styles.linkIcon} />
</Link>
</CursorEffect>
);
}

View File

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

View File

@@ -0,0 +1,89 @@
.card {
border-radius: 36px;
overflow: hidden;
background-color: #a4a4a41c;
transition: 0.3s ease;
backdrop-filter: blur(18px);
&:hover {
background-color: #3d3d3d1c;
}
a {
cursor: none;
}
}
.disableGlass {
background-color: #f5f5f5;
backdrop-filter: none;
&:hover {
background-color: #eaeaea;
}
}
.content {
display: flex;
flex-direction: row;
padding: 22px;
}
.qrCode {
width: 200px;
height: 200px;
margin-right: 20px;
flex-shrink: 0;
}
.textContainer {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.linkIcon {
width: 42px;
height: 42px;
margin-left: auto;
}
.getText {
color: #838383;
font-weight: 700;
font-size: 1.25rem;
margin-top: auto;
margin-bottom: -6px;
margin-right: 8px;
}
.storeNameText {
color: #242424;
font-weight: 700;
font-size: 2rem;
margin-right: 8px;
}
@media screen and (max-width: 1180px) {
.qrCode {
width: 180px;
height: 180px;
}
}
@media screen and (max-width: 980px) {
.qrCode {
display: none;
}
.content {
padding-left: 32px;
padding-bottom: 26px;
}
}
@media screen and (max-width: 620px) {
.content {
padding: 36px;
}
}

View File

@@ -0,0 +1,57 @@
import styles from './googlePlayButton.module.scss';
import Link, { LinkProps } from 'next/link';
import QRCodeSvg from '@/assets/images/qr-code.svg';
import LinkIcon from '@/assets/icons/link.svg';
import useLocale, { LocalesMap } from '@/utils/useLocale';
import clsx from 'clsx';
import { CursorEffect } from '@/components/cursor';
const locales: LocalesMap = {
ru: {
getText: 'Доступно в',
},
en: {
getText: 'Get it on',
},
};
type GooglePlayButtonProps = Omit<LinkProps, 'href'> & {
className?: string;
disableGlass?: boolean;
};
export function GooglePlayButton(props: GooglePlayButtonProps) {
const {
className: restCalssName,
disableGlass = false,
...restProps
} = props;
const t = useLocale(locales);
return (
<CursorEffect cursorBorderRadius={36} cursorPadding={12}>
<div
className={clsx(
styles.card,
disableGlass && styles.disableGlass,
restCalssName
)}
>
<Link
{...restProps}
target="_blank"
href="https://play.google.com/store/apps/details?id=org.xtimms.ridebus"
>
<div className={styles.content}>
<QRCodeSvg className={styles.qrCode} />
<div className={styles.textContainer}>
<LinkIcon className={styles.linkIcon} />
<span className={styles.getText}>{t('getText')}</span>
<span className={styles.storeNameText}>Google Play</span>
</div>
</div>
</Link>
</div>
</CursorEffect>
);
}

View File

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

View File

@@ -0,0 +1,13 @@
.rootWrapper {
}
.root {
font-size: 1rem;
font-weight: 700;
color: var(--on-background);
display: flex;
align-items: center;
gap: 10px;
padding: 18px;
cursor: none;
}

View File

@@ -0,0 +1,40 @@
import styles from './googlePlayLink.module.scss';
import Link, { LinkProps } from 'next/link';
import LinkIcon from '@/assets/icons/link.svg';
import useLocale, { LocalesMap } from '@/utils/useLocale';
import clsx from 'clsx';
import { CursorEffect } from '@/components/cursor';
const locales: LocalesMap = {
ru: {
getText: 'Доступно в Google Play',
},
en: {
getText: 'Get it on Google Play',
},
};
type GooglePlayLinkProps = Omit<LinkProps, 'href'> & { className?: string };
export function GooglePlayLink(props: GooglePlayLinkProps) {
const { className: restCalssName, ...restProps } = props;
const t = useLocale(locales);
return (
<CursorEffect
effectDistance={48}
effectForce={4}
cursorPadding={0}
className={clsx(styles.rootWrapper, restCalssName)}
>
<Link
{...restProps}
className={clsx(styles.root)}
target="_blank"
href="https://play.google.com/store/apps/details?id=org.xtimms.ridebus"
>
{t('getText')} <LinkIcon />
</Link>
</CursorEffect>
);
}

View File

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

View File

@@ -0,0 +1,18 @@
#ifdef GL_ES
precision mediump float;
#endif
const vec3 background = vec3(1.0);
const vec3 primary = vec3(0., 0.345, 0.761);
const vec3 tertiary = vec3(0.675, 0.514, 0.682);
varying float vHeight;
varying float vColor;
void main() {
float height = vHeight * 2.0 + 0.9;
vec3 color = mix(primary, tertiary, vColor * 3.0 + 0.5);
gl_FragColor = vec4(mix(color, background, height), 1.0);
}

View File

@@ -0,0 +1,116 @@
import { useEffect, useRef, useState } from 'react';
import styles from './gradients.module.css';
import { initGrainFilter } from './grainPostProcessing';
import { InitThreeOptions, initThree } from './initThree';
import { initScene } from './initScene';
import { initGradient } from './initGradients';
import Stats from 'stats.js';
const threeCanvas = (
canvas: HTMLCanvasElement,
viewportSize: InitThreeOptions,
initTimeOffset: number = 0
) => {
const { renderer, clock } = initThree(canvas, viewportSize);
const { scene, camera, updateSize: updateCameraSize } = initScene(renderer);
const {
render: renderGradient,
updateSize: updateGradientSize,
unload: unloadGradinet,
} = initGradient(scene, renderer);
const { render: renderFrame, resize: resizeFrame } = initGrainFilter(
renderer,
scene,
camera
);
//const stats = new Stats();
// the number will decide which information will be displayed
// 0 => FPS Frames rendered in the last second. The higher the number the better.
// 1 => MS Milliseconds needed to render a frame. The lower the number the better.
// 2 => MB MBytes of allocated memory. (Run Chrome with --enable-precise-memory-info)
// 3 => CUSTOM User-defined panel support.
//stats.showPanel(0);
//document.body.appendChild(stats.dom);
let elapsedTime = 0;
let delta = 0;
let isActive = true;
const animate = () => {
if (!isActive) return;
//stats.begin();
elapsedTime = clock.elapsedTime + initTimeOffset;
delta = clock.getDelta();
renderGradient(elapsedTime, delta);
renderFrame(elapsedTime, delta);
//stats.end();
requestAnimationFrame(animate);
};
const resizeCanvas = (width: number, height: number) => {
renderer.setSize(width, height);
updateCameraSize();
updateGradientSize();
resizeFrame();
};
animate();
resizeCanvas(viewportSize.width, viewportSize.height);
const unload = () => {
isActive = false;
unloadGradinet();
renderer.dispose();
};
return {
resizeCanvas,
unload,
};
};
export function Gradients() {
const rootRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [timeOffset] = useState(Math.random() * 1000);
useEffect(() => {
try {
const canvas = threeCanvas(
canvasRef.current as HTMLCanvasElement,
{
width: rootRef.current?.clientWidth || 0,
height: rootRef.current?.clientHeight || 0,
},
timeOffset
);
const resize = () => {
canvas.resizeCanvas(
rootRef.current?.clientWidth || 0,
rootRef.current?.clientHeight || 0
);
};
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
canvas.unload();
};
} catch (e) {
console.error(e);
}
}, [timeOffset]);
return (
<div className={styles.root} ref={rootRef}>
<canvas className={styles.canvas} ref={canvasRef}></canvas>
</div>
);
}

View File

@@ -0,0 +1,75 @@
#ifdef GL_ES
precision mediump float;
#endif
const float PI = 3.14159265359;
const float SCALE = 0.0002;
const float SPEED = 0.5;
const float NOISE_FREQ = 3.5;
const float NOISE_AMP = 0.3;
varying vec2 vUv;
varying float vHeight;
varying float vColor;
uniform float uTime;
uniform sampler2D map;
float rand(vec2 co, float l, float t) {
return fract(sin(dot(co.xy + vec2(l, t), vec2(12.9898, 78.233))) * 43758.5453);
}
float perlin(vec2 p, float dim, float time) {
vec2 pos = floor(p * dim);
vec2 posx = pos + vec2(1.0, 0.0);
vec2 posy = pos + vec2(0.0, 1.0);
vec2 posxy = pos + vec2(1.0);
float c = rand(pos, dim, time);
float cx = rand(posx, dim, time);
float cy = rand(posy, dim, time);
float cxy = rand(posxy, dim, time);
vec2 d = fract(p * dim);
d = -0.5 * cos(d * PI) + 0.5;
float ccx = mix(c, cx, d.x);
float cycxy = mix(cy, cxy, d.x);
float center = mix(ccx, cycxy, d.y);
return center * 2.0 - 1.0;
}
float snoiseFoam(vec2 pos, const float freq) {
return perlin(vec2(pos.y * freq, pos.x * freq), 9.0, 0.0);
}
float snoiseWater(vec2 pos, const float freq) {
return perlin(vec2(pos.y * freq, pos.x * freq), 6.0, 0.0);
}
mat2 rotate2d(float _angle) {
return mat2(cos(_angle), -sin(_angle), sin(_angle), cos(_angle));
}
void main() {
vUv = uv;
vec2 rTimeSpeed = vec2(uTime) * rotate2d(PI * (70.0 / 180.0)) * 0.15 * SPEED;
vec2 rTimeSlow = vec2(uTime) * rotate2d(PI * (250.0 / 180.0)) * 0.1 * SPEED;
vec2 rTimeMiddle = vec2(uTime + 100.0) * rotate2d(PI * (300.0 / 180.0)) * 0.12 * SPEED;
vec2 freqPos = position.xy * SCALE * NOISE_FREQ;
float waveBig = snoiseWater(freqPos + rTimeSlow, 0.08) * 0.4;
float waveMiddle = snoiseFoam(freqPos + rTimeMiddle, 0.1) * 0.3;
float waveSmall = snoiseFoam(freqPos + rTimeSpeed, 0.1) * 0.3;
vColor = waveMiddle;
vHeight = (waveBig + waveSmall + waveMiddle) * NOISE_AMP;
vec3 pos = position;
pos.z += vHeight / SCALE;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

View File

@@ -0,0 +1,14 @@
.root {
width: 100%;
height: 100%;
position: relative;
}
.canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -1;
}

View File

@@ -0,0 +1,7 @@
.main {
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
}

View File

@@ -0,0 +1,95 @@
uniform float uTime;
uniform sampler2D tDiffuse;
varying vec2 vUv;
#define SHOW_NOISE 0
#define SRGB 0
// 0: Addition, 1: Screen, 2: Overlay, 3: Soft Light, 4: Lighten-Only
#define BLEND_MODE 0
//0 to 1
#define SPEED 0.000001
#define INTENSITY 0.02
// What gray level noise should tend to.
#define MEAN 0.0
// Controls the contrast/variance of noise.
#define VARIANCE .3
float rand(vec2 co) {
return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
float rand(vec2 co, float l) {
return rand(vec2(rand(co), l));
}
float rand(vec2 co, float l, float t) {
return rand(vec2(rand(co, l), t));
}
vec3 channel_mix(vec3 a, vec3 b, vec3 w) {
return vec3(mix(a.r, b.r, w.r), mix(a.g, b.g, w.g), mix(a.b, b.b, w.b));
}
float gaussian(float z, float u, float o) {
return (1.0 / (o * sqrt(2.0 * 3.1415))) * exp(-(((z - u) * (z - u)) / (2.0 * (o * o))));
}
vec3 madd(vec3 a, vec3 b, float w) {
return a + a * b * w;
}
vec3 screen(vec3 a, vec3 b, float w) {
return mix(a, vec3(1.0) - (vec3(1.0) - a) * (vec3(1.0) - b), w);
}
vec3 overlay(vec3 a, vec3 b, float w) {
return mix(a, channel_mix(2.0 * a * b, vec3(1.0) - 2.0 * (vec3(1.0) - a) * (vec3(1.0) - b), step(vec3(0.5), a)), w);
}
vec3 soft_light(vec3 a, vec3 b, float w) {
return mix(a, pow(a, pow(vec3(2.0), 2.0 * (vec3(0.5) - b))), w);
}
void main() {
float t = uTime * float(SPEED);
vec2 coord = gl_FragCoord.xy;
vec2 ps = vec2(1.0) / gl_FragCoord.xy;
vec2 uv = coord * ps;
vec4 color = texture2D(tDiffuse, vUv);
#if SRGB
color = pow(color, vec4(2.2));
#endif
float noise = gaussian(rand(ps, 1.0 / (color.r + color.g + color.b), t), float(MEAN), float(VARIANCE) * float(VARIANCE));
#if SHOW_NOISE
color = vec4(noise);
gl_FragColor = vec4(vec3(0.0), color.r);
#else
float w = float(INTENSITY);
vec3 grain = vec3(noise);//vec3(noise) * (1.0 - color.rgb);
#if BLEND_MODE == 0
color.rgb += grain * w;
#elif BLEND_MODE == 1
color.rgb = screen(color.rgb, grain, w);
#elif BLEND_MODE == 2
color.rgb = overlay(color.rgb, grain, w);
#elif BLEND_MODE == 3
color.rgb = soft_light(color.rgb, grain, w);
#elif BLEND_MODE == 4
color.rgb = max(color.rgb, grain * w);
#endif
#if SRGB
color = pow(color, vec4(1.0 / 2.2));
#endif
gl_FragColor = color;
#endif
}

View File

@@ -0,0 +1,46 @@
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { GrainShader } from './grainPostprocessingShader';
export function initGrainFilter(
renderer: THREE.WebGLRenderer,
scene: THREE.Scene,
camera: THREE.OrthographicCamera
) {
const width = window.innerWidth || 2;
const height = window.innerHeight || 2;
const effectFilm = new ShaderPass(GrainShader);
const rtParameters = {
stencilBuffer: true,
};
const renderScenePass = new RenderPass(scene, camera);
let composer2 = new EffectComposer(
renderer,
new THREE.WebGLRenderTarget(width, height, rtParameters)
);
composer2.setPixelRatio(window.devicePixelRatio);
composer2.addPass(renderScenePass);
composer2.addPass(effectFilm);
return {
render: (elapsed: number, delta: number) => {
composer2.render(delta);
effectFilm.uniforms['uTime'].value = elapsed;
},
resize: () => {
const viewportSize = renderer.getSize(new THREE.Vector2());
composer2.setSize(viewportSize.width, viewportSize.height);
},
};
}

View File

@@ -0,0 +1,30 @@
import grainFragmentShader from './grain.fragmentShader.glsl?raw';
/**
* Full-screen textured quad shader
*/
const GrainShader = {
name: 'GrainShader',
uniforms: {
tDiffuse: { value: null },
opacity: { value: 1.0 },
uTime: { value: 0 },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: grainFragmentShader,
};
export { GrainShader };

View File

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

View File

@@ -0,0 +1,57 @@
import * as THREE from 'three';
import gradientFragmentShader from './gradient.fragmentShader.glsl?raw';
import gradientVertexShader from './gradient.vertexShader.glsl?raw';
function getMaterial() {
var uniforms = {
uTime: {
value: 0,
},
};
var material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: gradientVertexShader,
fragmentShader: gradientFragmentShader,
//wireframe: true,
});
return material;
}
export function initGradient(
scene: THREE.Scene,
renderer: THREE.WebGLRenderer
) {
const quality = 0.02;
const material = getMaterial();
const plane = new THREE.Mesh(new THREE.PlaneGeometry(), material);
scene.add(plane);
const render = (elapsed: number, delta: number) => {
material.uniforms.uTime.value = elapsed;
};
const updateSize = () => {
const viewportSize = renderer.getSize(new THREE.Vector2());
plane.geometry.dispose();
plane.geometry = new THREE.PlaneGeometry(
viewportSize.width,
viewportSize.height * 4,
viewportSize.width * quality,
viewportSize.height * 4 * quality
);
};
updateSize();
const unload = () => {
scene.remove(plane);
plane.geometry.dispose();
material.dispose();
};
return { render, updateSize, unload };
}

View File

@@ -0,0 +1,26 @@
import * as THREE from 'three';
export function initScene(renderer: THREE.WebGLRenderer) {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
const camera = new THREE.OrthographicCamera(-1, -1, -1, -1, -100000, 100000);
camera.position.z = 5;
camera.position.y = 8;
camera.lookAt(scene.position);
const updateSize = () => {
const viewportSize = renderer.getSize(new THREE.Vector2());
camera.left = viewportSize.width / -2;
camera.right = viewportSize.width / 2;
camera.top = viewportSize.height / 2;
camera.bottom = -viewportSize.height / 2;
camera.updateProjectionMatrix();
};
updateSize();
return { scene, camera, updateSize };
}

View File

@@ -0,0 +1,19 @@
import * as THREE from 'three';
export type InitThreeOptions = {
width: number;
height: number;
};
export function initThree(
canvas: HTMLCanvasElement,
{ width, height }: InitThreeOptions
) {
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
const clock = new THREE.Clock();
return { renderer, clock };
}

View File

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

View File

@@ -0,0 +1,102 @@
.langSwitcherRoot {
}
.langSwitcher {
position: relative;
z-index: 1000;
}
.currentLanguage {
border: none;
background: none;
font: inherit;
font-size: 1rem;
font-weight: 700;
color: var(--on-background);
display: flex;
align-items: center;
gap: 10px;
padding: 18px;
cursor: none;
}
.selector {
position: absolute;
width: 200px;
top: 0;
right: 0;
background-color: var(--background);
padding: 8px 8px;
border-radius: 16px;
border: 1px solid var(--divider);
box-shadow: 0px 4px 16px #00000014;
background-color: #fafafa;
z-index: 1000;
pointer-events: none;
opacity: 0;
.header {
color: #6f6f6f;
font-weight: 500;
font-size: 14px;
margin: 4px 16px;
margin-bottom: 8px;
}
ul {
list-style: none;
button {
font: inherit;
border: none;
background: none;
width: 100%;
text-align: left;
font-size: 1rem;
font-weight: 600;
padding: 6px 16px;
cursor: none !important;
border-radius: 8px;
transition: 0.3s ease;
&:hover {
background-color: var(--divider);
}
}
}
}
.openSelector {
pointer-events: all;
opacity: 1;
}
.hideCurrent {
opacity: 0;
}
@media screen and (max-width: 570px) {
.currentLangLabel {
display: none;
}
}
@media screen and (max-width: 540px) {
.selector {
width: calc(100vw - 48px);
right: -24px;
box-shadow: 0px 4px 180px rgb(0 0 0 / 31%);
.header {
font-size: 16px;
}
ul {
li {
padding: 12px 16px;
}
}
}
}

View File

@@ -0,0 +1,99 @@
import useLocale, { LocalesMap } from '@/utils/useLocale';
import { clsx } from 'clsx';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
import styles from './langSwitcher.module.scss';
import LanguageIcon from '@/assets/icons/language.svg';
import { CursorEffect } from '@/components/cursor';
type LangSwitcherProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {};
const locales: LocalesMap = {
ru: {
chooseHeader: 'Выберите язык',
},
en: {
chooseHeader: 'Choose language',
},
};
const localesMap: { [key: string]: string } = {
en: 'English',
ru: 'Русский',
};
export function LangSwitcher(props: LangSwitcherProps) {
const { className: restClassName, ...restProps } = props;
const router = useRouter();
const selectorRef = useRef(null);
const buttonRef = useRef(null);
const [open, setOpen] = useState(false);
const t = useLocale(locales);
const { pathname, asPath, query } = router;
useEffect(() => {
if (!open) return;
const handleClose = (event: any) => {
setOpen(
event.composedPath().includes(selectorRef.current) ||
event.composedPath().includes(buttonRef.current)
);
};
addEventListener('click', handleClose, false);
return () => removeEventListener('click', handleClose, false);
}, [open]);
const handleSetLanguage = (locale: string) => () => {
router.push({ pathname, query }, asPath, { locale });
setOpen(false);
};
return (
<div className={clsx(styles.langSwitcher, restClassName)} {...restProps}>
<CursorEffect
effectDistance={48}
effectForce={4}
cursorPadding={0}
className={clsx(styles.langSwitcherRoot)}
>
<button
ref={buttonRef}
className={clsx(styles.currentLanguage, open && styles.hideCurrent)}
onClick={() => setOpen(true)}
>
<LanguageIcon />{' '}
<span className={styles.currentLangLabel}>
{localesMap[router.locale || 'en']}
</span>
</button>
</CursorEffect>
<div
ref={selectorRef}
className={clsx(styles.selector, open && styles.openSelector)}
>
<h5 className={styles.header}>{t('chooseHeader')}</h5>
<ul>
{Object.keys(localesMap).map((locale) => (
<CursorEffect
key={locale}
effectForce={2}
effectDistance={36}
cursorPadding={2}
cursorBorderRadius={8}
>
<button onClick={handleSetLanguage(locale)}>
{localesMap[locale]}
</button>
</CursorEffect>
))}
</ul>
</div>
</div>
);
}

View File

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

View File

@@ -0,0 +1,31 @@
.logo {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.vertical {
flex-direction: column;
}
.icon {
width: 60px;
}
.title {
font-weight: 800;
font-size: 1.75rem;
}
@media screen and (max-width: 640px) {
.logo {
gap: 8px;
}
.icon {
width: 40px;
}
.title {
font-size: 1.25rem;
}
}

57
components/logo/logo.tsx Normal file
View File

@@ -0,0 +1,57 @@
import { clsx } from 'clsx';
import styles from './logo.module.css';
import LogoSVG from '@/assets/images/logo-alt.svg';
import useLocale, { LocalesMap } from '@/utils/useLocale';
export enum LogoType {
Full,
Title,
Icon,
}
export enum Orientation {
Horizontal,
Vertical,
}
type LogoProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
orientation?: Orientation;
variant?: LogoType;
};
const localesMap: LocalesMap = {
ru: {
title: 'ridebus',
},
en: {
title: 'ridebus',
},
};
export function Logo(props: LogoProps) {
const {
orientation = Orientation.Horizontal,
variant = LogoType.Full,
className: restClassName,
...restProps
} = props;
const t = useLocale(localesMap);
return (
<div
className={clsx(
restClassName,
styles.logo,
orientation === Orientation.Vertical && styles.vertical
)}
{...restProps}
>
{variant !== LogoType.Title && <LogoSVG className={styles.icon} />}
{variant !== LogoType.Icon && (
<h1 className={styles.title}>{t('title')}</h1>
)}
</div>
);
}

View File

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

View File

@@ -0,0 +1,53 @@
.root {
position: absolute;
max-width: 1400px;
left: 0;
top: 100px;
right: 0;
margin: auto;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
padding: 0 48px;
gap: 0px;
z-index: 100;
}
.homeLink {
padding: 8px;
padding-right: 24px;
border-radius: 20px;
display: block;
margin-right: auto;
margin-left: -8px;
cursor: none !important;
}
.logo {
margin-right: auto;
}
@media screen and (max-width: 1100px) {
.root {
top: 70px;
}
}
@media screen and (max-width: 800px) {
.googlePlayLink {
display: none !important;
}
}
@media screen and (max-width: 640px) {
.root {
top: 30px;
}
.topBar {
top: 20px;
padding: 0 36px;
gap: 36px;
}
}

View File

@@ -0,0 +1,52 @@
import styles from './topBar.module.scss';
import { LangSwitcher } from '@/components/langSwitcher';
import { Logo, LogoType } from '@/components/logo';
import { GithubLink } from '@/components/githubLink';
import { GooglePlayLink } from '@/components/googlePlayLink';
import { useRef } from 'react';
import useScroll from '@/utils/useScroll';
import { CursorEffect } from '@/components/cursor';
import Link from 'next/link';
import { useRouter } from 'next/router';
type TopBarProps = {
notHide?: boolean;
};
export function TopBar(props: TopBarProps) {
const { notHide = false } = props;
const router = useRouter();
const rootRef = useRef<HTMLDivElement>(null);
useScroll((scrollY, deltaTime, time) => {
if (notHide) {
return;
}
if (rootRef.current) {
rootRef.current.style.transform = `translate3D(0, ${Math.max(-Math.expm1(scrollY / 6), -400)}px, 1000px)`;
}
});
return (
<div className={styles.root} ref={rootRef}>
{router.pathname === '/' && (
<Logo className={styles.logo} variant={LogoType.Full} />
)}
{router.pathname !== '/' && (
<CursorEffect
cursorPadding={4}
cursorBorderRadius={26}
className={styles.logo}
>
<Link href="/" className={styles.homeLink}>
<Logo variant={LogoType.Full} />
</Link>
</CursorEffect>
)}
<GooglePlayLink className={styles.googlePlayLink} />
<GithubLink />
<LangSwitcher />
</div>
);
}