add eslint, bug fixes

This commit is contained in:
Salam-vsem
2025-05-18 00:56:02 +03:00
parent bc645a8261
commit 33a432cbd1
18 changed files with 3264 additions and 32 deletions

4
.gitignore vendored
View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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"

View File

@@ -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,
}, },
}} }}
> >

View File

@@ -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"

View File

@@ -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'

View File

@@ -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'
)} )}
> >

View File

@@ -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]"

View File

@@ -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,
}} }}

View File

@@ -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'

View File

@@ -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(),

View File

@@ -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
View 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
}

View File

@@ -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>
) )
} }

View File

@@ -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;

View File

@@ -23,7 +23,7 @@ export default {
}, },
}, },
aspectRatio: { aspectRatio: {
'3/4': '3 / 4' '3/4': '3 / 4',
}, },
keyframes: { keyframes: {
marquee: { marquee: {