add eslint, bug fixes
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,3 +22,7 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# ESLint
|
||||||
|
.eslintcache
|
||||||
|
.eslintrc.*.backup
|
||||||
|
|||||||
49
eslint.config.js
Normal file
49
eslint.config.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import eslintPluginAstro from 'eslint-plugin-astro'
|
||||||
|
import typescriptParser from '@typescript-eslint/parser'
|
||||||
|
import typescriptPlugin from '@typescript-eslint/eslint-plugin'
|
||||||
|
import reactPlugin from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...eslintPluginAstro.configs.recommended,
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'node_modules/**',
|
||||||
|
'dist/**',
|
||||||
|
'.astro/**',
|
||||||
|
'coverage/**',
|
||||||
|
'*.config.{js,ts,mjs}',
|
||||||
|
'.prettierrc',
|
||||||
|
'package*.json',
|
||||||
|
'tsconfig.json',
|
||||||
|
],
|
||||||
|
files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': typescriptPlugin,
|
||||||
|
react: reactPlugin,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parser: typescriptParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...typescriptPlugin.configs.recommended.rules,
|
||||||
|
'react/jsx-uses-react': 'error',
|
||||||
|
'react/jsx-uses-vars': 'error',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||||
|
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
3127
package-lock.json
generated
3127
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@
|
|||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check ."
|
"format:check": "prettier --check .",
|
||||||
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro --cache --max-warnings 0",
|
||||||
|
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx,.astro --fix --cache"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "^4.2.7",
|
"@astrojs/react": "^4.2.7",
|
||||||
@@ -24,6 +26,11 @@
|
|||||||
"tailwindcss": "^3.4.17"
|
"tailwindcss": "^3.4.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||||
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
|
"eslint": "^9.27.0",
|
||||||
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11"
|
"prettier-plugin-tailwindcss": "^0.6.11"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const t = useTranslations(lang)
|
|||||||
|
|
||||||
<footer id="contacts" class="flex flex-col items-center gap-4 text-center md:gap-6">
|
<footer id="contacts" class="flex flex-col items-center gap-4 text-center md:gap-6">
|
||||||
<script
|
<script
|
||||||
|
script-src:unsafe-inline
|
||||||
is:inline
|
is:inline
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
charset="utf-8"
|
charset="utf-8"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SwiperSlide } from 'swiper/react'
|
|||||||
import StarIconSrc from '@/assets/icons/StarIcon.svg'
|
import StarIconSrc from '@/assets/icons/StarIcon.svg'
|
||||||
import { useTranslations } from '@/i18n/utils'
|
import { useTranslations } from '@/i18n/utils'
|
||||||
import { SimpleSwiper } from './swiper/SimpleSwiper'
|
import { SimpleSwiper } from './swiper/SimpleSwiper'
|
||||||
|
import { isAboveBreakpoint } from '@/libs/breakpoint'
|
||||||
|
|
||||||
type Review = z.infer<ReturnType<typeof reviewsSchema>>
|
type Review = z.infer<ReturnType<typeof reviewsSchema>>
|
||||||
type Author = Review['author']
|
type Author = Review['author']
|
||||||
@@ -18,6 +19,7 @@ type ReviewsProps = {
|
|||||||
const REVIEWS_LINK =
|
const REVIEWS_LINK =
|
||||||
'https://yandex.ru/maps/213/moscow/?ll=37.469982%2C55.781185&mode=poi&poi%5Bpoint%5D=37.469074%2C55.781319&poi%5Buri%5D=ymapsbm1%3A%2F%2Forg%3Foid%3D19715751392%26yclid%3D10576664134676381695%26banner_id%3D1873406736351323707&tab=reviews&z=17.75'
|
'https://yandex.ru/maps/213/moscow/?ll=37.469982%2C55.781185&mode=poi&poi%5Bpoint%5D=37.469074%2C55.781319&poi%5Buri%5D=ymapsbm1%3A%2F%2Forg%3Foid%3D19715751392%26yclid%3D10576664134676381695%26banner_id%3D1873406736351323707&tab=reviews&z=17.75'
|
||||||
const swiperSlideClassName = '!h-auto md:max-w-[600px]'
|
const swiperSlideClassName = '!h-auto md:max-w-[600px]'
|
||||||
|
const isMd = isAboveBreakpoint('MD')
|
||||||
|
|
||||||
export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
||||||
const t = useTranslations(lang)
|
const t = useTranslations(lang)
|
||||||
@@ -25,11 +27,10 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
|||||||
return (
|
return (
|
||||||
<SimpleSwiper
|
<SimpleSwiper
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
freeMode={false}
|
freeMode={isMd}
|
||||||
breakpoints={{
|
breakpoints={{
|
||||||
768: {
|
768: {
|
||||||
slidesPerView: 'auto',
|
slidesPerView: 'auto',
|
||||||
freeMode: true,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useTranslations } from '@/i18n/utils'
|
|
||||||
import type { CollectionEntry } from 'astro:content'
|
import type { CollectionEntry } from 'astro:content'
|
||||||
import rightArrowIconSrc from '@/assets/icons/RightArrowIcon.svg'
|
import rightArrowIconSrc from '@/assets/icons/RightArrowIcon.svg'
|
||||||
|
|
||||||
export type CoachCardProps = CollectionEntry<'coaches'> & { lang?: string }
|
export type CoachCardProps = CollectionEntry<'coaches'> & { lang?: string }
|
||||||
|
|
||||||
export const CoachCard: React.FC<CoachCardProps> = ({ data, lang, slug }) => {
|
export const CoachCard: React.FC<CoachCardProps> = ({ data, slug }) => {
|
||||||
const { name, description, cover, coverAlt } = data
|
const { name, cover, coverAlt } = data
|
||||||
const t = useTranslations(lang)
|
|
||||||
const coachPageLink = `/coaches/${slug}`
|
const coachPageLink = `/coaches/${slug}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="aspect-3/4 relative h-full max-w-[262px] overflow-hidden rounded-3xl text-white md:max-w-[320px] lg:max-w-[400px]">
|
<div className="relative aspect-3/4 h-full max-w-[262px] overflow-hidden rounded-3xl text-white md:max-w-[320px] lg:max-w-[400px]">
|
||||||
<img
|
<img
|
||||||
{...cover}
|
{...cover}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CollectionEntry, z } from 'astro:content'
|
import type { CollectionEntry } from 'astro:content'
|
||||||
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
||||||
import { SwiperSlide } from 'swiper/react'
|
import { SwiperSlide } from 'swiper/react'
|
||||||
import { CoachCard } from './CoachCard'
|
import { CoachCard } from './CoachCard'
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const MobileNavigation: React.FC<MobileNavigationProps> = ({ navItems }) => {
|
|||||||
<AnimatedBurgerButton isOpen={isOpen} onClick={toggleMenu} />
|
<AnimatedBurgerButton isOpen={isOpen} onClick={toggleMenu} />
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed left-0 top-0 z-50 w-full h-[100dvh] transform bg-bg-main p-8 pt-16 transition-transform duration-300 ease-in-out',
|
'fixed left-0 top-0 z-50 h-[100dvh] w-full transform bg-bg-main p-8 pt-16 transition-transform duration-300 ease-in-out',
|
||||||
isOpen ? '-translate-x-0' : 'translate-x-full'
|
isOpen ? '-translate-x-0' : 'translate-x-full'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { servicesSchema } from '@/content.config'
|
import type { servicesSchema } from '@/content.config'
|
||||||
import type { z } from 'astro:content'
|
import type { z } from 'astro:content'
|
||||||
import { useState } from 'react'
|
import { useCallback, useRef, useState } from 'react'
|
||||||
import { SwiperSlide } from 'swiper/react'
|
import { type SwiperRef, SwiperSlide } from 'swiper/react'
|
||||||
import { CharachteristicCard } from './CharachteristicCard'
|
import { CharachteristicCard } from './CharachteristicCard'
|
||||||
import { cn } from '@/libs/cn'
|
import { cn } from '@/libs/cn'
|
||||||
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
||||||
@@ -11,9 +11,14 @@ export type ServicesProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Services: React.FC<ServicesProps> = ({ services }) => {
|
export const Services: React.FC<ServicesProps> = ({ services }) => {
|
||||||
|
const swiperRef = useRef<SwiperRef | null>(null)
|
||||||
const [serviceId, setServiceId] = useState(0)
|
const [serviceId, setServiceId] = useState(0)
|
||||||
const selectedService = services[serviceId]
|
const selectedService = services[serviceId]
|
||||||
const isSelected = (index: number) => index === serviceId
|
const isSelected = (index: number) => index === serviceId
|
||||||
|
const updateServiceHandler = useCallback((index: number) => {
|
||||||
|
setServiceId(index)
|
||||||
|
swiperRef.current?.swiper.slideTo(0)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
@@ -22,14 +27,14 @@ export const Services: React.FC<ServicesProps> = ({ services }) => {
|
|||||||
<SwiperSlide style={{ width: 'auto' }} key={index}>
|
<SwiperSlide style={{ width: 'auto' }} key={index}>
|
||||||
<button
|
<button
|
||||||
className={cn('tag-btn', isSelected(index) && 'tag-btn-active')}
|
className={cn('tag-btn', isSelected(index) && 'tag-btn-active')}
|
||||||
onClick={() => setServiceId(index)}
|
onClick={() => updateServiceHandler(index)}
|
||||||
>
|
>
|
||||||
{service.title}
|
{service.title}
|
||||||
</button>
|
</button>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
</SimpleSwiper>
|
</SimpleSwiper>
|
||||||
<SimpleSwiper>
|
<SimpleSwiper ref={swiperRef}>
|
||||||
{selectedService.charachteristics?.map((charachteristic, index) => (
|
{selectedService.charachteristics?.map((charachteristic, index) => (
|
||||||
<SwiperSlide
|
<SwiperSlide
|
||||||
className="aspect-3/4 !h-auto w-full max-w-[262px] md:max-w-[320px]"
|
className="aspect-3/4 !h-auto w-full max-w-[262px] md:max-w-[320px]"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const MediaSwiper = React.forwardRef<SwiperRef, MediaSwiperProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
modules={[Navigation, Pagination, A11y, ...modules]}
|
modules={[Navigation, Pagination, A11y, ...modules]}
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
|
spaceBetween={24}
|
||||||
pagination={{
|
pagination={{
|
||||||
clickable: true,
|
clickable: true,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { FreeMode, Mousewheel } from 'swiper/modules'
|
import { FreeMode, Mousewheel } from 'swiper/modules'
|
||||||
import { Swiper, type SwiperProps } from 'swiper/react'
|
import { Swiper, type SwiperProps, type SwiperRef } from 'swiper/react'
|
||||||
import 'swiper/css'
|
import 'swiper/css'
|
||||||
import 'swiper/css/free-mode'
|
import 'swiper/css/free-mode'
|
||||||
import './simple-swiper.css'
|
import './simple-swiper.css'
|
||||||
import { cn } from '@/libs/cn'
|
import { cn } from '@/libs/cn'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
export const SimpleSwiper: React.FC<SwiperProps> = ({ children, className, ...props }) => (
|
export const SimpleSwiper = React.forwardRef<SwiperRef, SwiperProps>(
|
||||||
|
({ children, className, ...props }, ref) => (
|
||||||
<Swiper
|
<Swiper
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
slidesPerView="auto"
|
slidesPerView="auto"
|
||||||
@@ -14,8 +16,11 @@ export const SimpleSwiper: React.FC<SwiperProps> = ({ children, className, ...pr
|
|||||||
mousewheel={{ enabled: true, forceToAxis: true }}
|
mousewheel={{ enabled: true, forceToAxis: true }}
|
||||||
modules={[FreeMode, Mousewheel]}
|
modules={[FreeMode, Mousewheel]}
|
||||||
{...props}
|
{...props}
|
||||||
|
ref={ref}
|
||||||
className={cn('simple-swiper', className)}
|
className={cn('simple-swiper', className)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
SimpleSwiper.displayName = 'SimpleSwiper'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineCollection, reference, z, type ImageFunction } from 'astro:content'
|
import { defineCollection, z, type ImageFunction } from 'astro:content'
|
||||||
|
|
||||||
export const socialMediaLinkSchema = z.object({
|
export const socialMediaLinkSchema = z.object({
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export function getLangFromUrl(url: URL) {
|
|||||||
|
|
||||||
if (lang in ui) return lang as keyof typeof ui
|
if (lang in ui) return lang as keyof typeof ui
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return i18n!.defaultLocale
|
return i18n!.defaultLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
src/libs/breakpoint.ts
Normal file
33
src/libs/breakpoint.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { TW_WIDTHS } from '@/constants/images'
|
||||||
|
|
||||||
|
type Breakpoint = keyof typeof TW_WIDTHS
|
||||||
|
|
||||||
|
export function getBreakpoint(): Breakpoint {
|
||||||
|
if (typeof window === 'undefined') return 'XXS'
|
||||||
|
|
||||||
|
const width = window.innerWidth
|
||||||
|
|
||||||
|
if (width >= TW_WIDTHS._2XL) return '_2XL'
|
||||||
|
if (width >= TW_WIDTHS.XL) return 'XL'
|
||||||
|
if (width >= TW_WIDTHS.LG) return 'LG'
|
||||||
|
if (width >= TW_WIDTHS.MD) return 'MD'
|
||||||
|
if (width >= TW_WIDTHS.SM) return 'SM'
|
||||||
|
if (width >= TW_WIDTHS.XS) return 'XS'
|
||||||
|
return 'XXS'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAboveBreakpoint(size: Breakpoint): boolean {
|
||||||
|
const currentBreakpoint = getBreakpoint()
|
||||||
|
const breakpoints = Object.keys(TW_WIDTHS) as Breakpoint[]
|
||||||
|
const currentIndex = breakpoints.indexOf(currentBreakpoint)
|
||||||
|
const targetIndex = breakpoints.indexOf(size)
|
||||||
|
return currentIndex >= targetIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBelowBreakpoint(size: Breakpoint): boolean {
|
||||||
|
const currentBreakpoint = getBreakpoint()
|
||||||
|
const breakpoints = Object.keys(TW_WIDTHS) as Breakpoint[]
|
||||||
|
const currentIndex = breakpoints.indexOf(currentBreakpoint)
|
||||||
|
const targetIndex = breakpoints.indexOf(size)
|
||||||
|
return currentIndex < targetIndex
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ const t = useTranslations(lang)
|
|||||||
{
|
{
|
||||||
optimizedReviews && (
|
optimizedReviews && (
|
||||||
<Section id="reviews" title={t('reviews.title')}>
|
<Section id="reviews" title={t('reviews.title')}>
|
||||||
<Reviews client:visible reviews={optimizedReviews} lang={lang} />
|
<Reviews client:only="react" reviews={optimizedReviews} lang={lang} />
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
@apply rounded-full border border-brand bg-transparent px-6 py-3 text-center text-text-light transition-colors duration-300 hover:bg-brand;
|
@apply rounded-full border border-brand bg-transparent px-6 py-3 text-center text-text-light transition-colors duration-300 hover:bg-brand;
|
||||||
}
|
}
|
||||||
.tag-btn {
|
.tag-btn {
|
||||||
@apply text-sm hover:tag-btn-active rounded-full bg-card p-2 transition-colors duration-300;
|
@apply hover:tag-btn-active rounded-full bg-card p-2 text-sm transition-colors duration-300;
|
||||||
}
|
}
|
||||||
.tag-btn-active {
|
.tag-btn-active {
|
||||||
@apply bg-brand text-text-light;
|
@apply bg-brand text-text-light;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
aspectRatio: {
|
aspectRatio: {
|
||||||
'3/4': '3 / 4'
|
'3/4': '3 / 4',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
marquee: {
|
marquee: {
|
||||||
|
|||||||
Reference in New Issue
Block a user