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
|
||||
.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",
|
||||
"astro": "astro",
|
||||
"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": {
|
||||
"@astrojs/react": "^4.2.7",
|
||||
@@ -24,6 +26,11 @@
|
||||
"tailwindcss": "^3.4.17"
|
||||
},
|
||||
"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-plugin-astro": "^0.14.1",
|
||||
"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">
|
||||
<script
|
||||
script-src:unsafe-inline
|
||||
is:inline
|
||||
type="text/javascript"
|
||||
charset="utf-8"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SwiperSlide } from 'swiper/react'
|
||||
import StarIconSrc from '@/assets/icons/StarIcon.svg'
|
||||
import { useTranslations } from '@/i18n/utils'
|
||||
import { SimpleSwiper } from './swiper/SimpleSwiper'
|
||||
import { isAboveBreakpoint } from '@/libs/breakpoint'
|
||||
|
||||
type Review = z.infer<ReturnType<typeof reviewsSchema>>
|
||||
type Author = Review['author']
|
||||
@@ -18,6 +19,7 @@ type ReviewsProps = {
|
||||
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'
|
||||
const swiperSlideClassName = '!h-auto md:max-w-[600px]'
|
||||
const isMd = isAboveBreakpoint('MD')
|
||||
|
||||
export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
||||
const t = useTranslations(lang)
|
||||
@@ -25,11 +27,10 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
||||
return (
|
||||
<SimpleSwiper
|
||||
slidesPerView={1}
|
||||
freeMode={false}
|
||||
freeMode={isMd}
|
||||
breakpoints={{
|
||||
768: {
|
||||
slidesPerView: 'auto',
|
||||
freeMode: true,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useTranslations } from '@/i18n/utils'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import rightArrowIconSrc from '@/assets/icons/RightArrowIcon.svg'
|
||||
|
||||
export type CoachCardProps = CollectionEntry<'coaches'> & { lang?: string }
|
||||
|
||||
export const CoachCard: React.FC<CoachCardProps> = ({ data, lang, slug }) => {
|
||||
const { name, description, cover, coverAlt } = data
|
||||
const t = useTranslations(lang)
|
||||
export const CoachCard: React.FC<CoachCardProps> = ({ data, slug }) => {
|
||||
const { name, cover, coverAlt } = data
|
||||
const coachPageLink = `/coaches/${slug}`
|
||||
|
||||
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
|
||||
{...cover}
|
||||
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 { SwiperSlide } from 'swiper/react'
|
||||
import { CoachCard } from './CoachCard'
|
||||
|
||||
@@ -66,7 +66,7 @@ const MobileNavigation: React.FC<MobileNavigationProps> = ({ navItems }) => {
|
||||
<AnimatedBurgerButton isOpen={isOpen} onClick={toggleMenu} />
|
||||
<div
|
||||
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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { servicesSchema } from '@/content.config'
|
||||
import type { z } from 'astro:content'
|
||||
import { useState } from 'react'
|
||||
import { SwiperSlide } from 'swiper/react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { type SwiperRef, SwiperSlide } from 'swiper/react'
|
||||
import { CharachteristicCard } from './CharachteristicCard'
|
||||
import { cn } from '@/libs/cn'
|
||||
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
||||
@@ -11,9 +11,14 @@ export type ServicesProps = {
|
||||
}
|
||||
|
||||
export const Services: React.FC<ServicesProps> = ({ services }) => {
|
||||
const swiperRef = useRef<SwiperRef | null>(null)
|
||||
const [serviceId, setServiceId] = useState(0)
|
||||
const selectedService = services[serviceId]
|
||||
const isSelected = (index: number) => index === serviceId
|
||||
const updateServiceHandler = useCallback((index: number) => {
|
||||
setServiceId(index)
|
||||
swiperRef.current?.swiper.slideTo(0)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
@@ -22,14 +27,14 @@ export const Services: React.FC<ServicesProps> = ({ services }) => {
|
||||
<SwiperSlide style={{ width: 'auto' }} key={index}>
|
||||
<button
|
||||
className={cn('tag-btn', isSelected(index) && 'tag-btn-active')}
|
||||
onClick={() => setServiceId(index)}
|
||||
onClick={() => updateServiceHandler(index)}
|
||||
>
|
||||
{service.title}
|
||||
</button>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</SimpleSwiper>
|
||||
<SimpleSwiper>
|
||||
<SimpleSwiper ref={swiperRef}>
|
||||
{selectedService.charachteristics?.map((charachteristic, index) => (
|
||||
<SwiperSlide
|
||||
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}
|
||||
modules={[Navigation, Pagination, A11y, ...modules]}
|
||||
slidesPerView={1}
|
||||
spaceBetween={24}
|
||||
pagination={{
|
||||
clickable: true,
|
||||
}}
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
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/free-mode'
|
||||
import './simple-swiper.css'
|
||||
import { cn } from '@/libs/cn'
|
||||
import React from 'react'
|
||||
|
||||
export const SimpleSwiper: React.FC<SwiperProps> = ({ children, className, ...props }) => (
|
||||
<Swiper
|
||||
direction="horizontal"
|
||||
slidesPerView="auto"
|
||||
freeMode={true}
|
||||
spaceBetween={24}
|
||||
mousewheel={{ enabled: true, forceToAxis: true }}
|
||||
modules={[FreeMode, Mousewheel]}
|
||||
{...props}
|
||||
className={cn('simple-swiper', className)}
|
||||
>
|
||||
{children}
|
||||
</Swiper>
|
||||
export const SimpleSwiper = React.forwardRef<SwiperRef, SwiperProps>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<Swiper
|
||||
direction="horizontal"
|
||||
slidesPerView="auto"
|
||||
freeMode={true}
|
||||
spaceBetween={24}
|
||||
mousewheel={{ enabled: true, forceToAxis: true }}
|
||||
modules={[FreeMode, Mousewheel]}
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn('simple-swiper', className)}
|
||||
>
|
||||
{children}
|
||||
</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({
|
||||
icon: z.string(),
|
||||
|
||||
@@ -8,6 +8,7 @@ export function getLangFromUrl(url: URL) {
|
||||
|
||||
if (lang in ui) return lang as keyof typeof ui
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
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 && (
|
||||
<Section id="reviews" title={t('reviews.title')}>
|
||||
<Reviews client:visible reviews={optimizedReviews} lang={lang} />
|
||||
<Reviews client:only="react" reviews={optimizedReviews} lang={lang} />
|
||||
</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;
|
||||
}
|
||||
.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 {
|
||||
@apply bg-brand text-text-light;
|
||||
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
},
|
||||
},
|
||||
aspectRatio: {
|
||||
'3/4': '3 / 4'
|
||||
'3/4': '3 / 4',
|
||||
},
|
||||
keyframes: {
|
||||
marquee: {
|
||||
|
||||
Reference in New Issue
Block a user