Skip to content

Commit 1e850b4

Browse files
committed
feat(weather): integrate Swiper for weather forecast slider and enhance styling
1 parent 324b68b commit 1e850b4

File tree

5 files changed

+108
-96
lines changed

5 files changed

+108
-96
lines changed

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"react-icons": "5.4.0",
2929
"react-router-dom": "^7.2.0",
3030
"react-select": "^5.10.0",
31+
"swiper": "^11.2.5",
3132
"uuid": "^11.1.0",
3233
"vite-plugin-pwa": "0.21.1",
3334
"vite-plugin-web-extension": "^4.4.3",

src/index.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,32 @@ input[type=number]::-webkit-outer-spin-button {
219219
/* Hide the spinner in Firefox */
220220
input[type=number] {
221221
-moz-appearance: textfield;
222+
}
223+
224+
225+
226+
227+
/* Add this to your CSS file */
228+
.weather-forecast-slider {
229+
padding-bottom: 25px !important;
230+
/* Space for pagination bullets */
231+
}
232+
233+
.weather-forecast-slider .swiper-slide {
234+
width: auto;
235+
max-width: 150px;
236+
}
237+
238+
.weather-forecast-slider .swiper-pagination {
239+
bottom: 0;
240+
}
241+
242+
.weather-forecast-slider .swiper-pagination-bullet {
243+
background-color: rgba(147, 197, 253, 0.7);
244+
/* blue-300 with opacity */
245+
}
246+
247+
.weather-forecast-slider .swiper-pagination-bullet-active {
248+
background-color: rgb(96, 165, 250);
249+
/* blue-400 */
222250
}

src/layouts/weather/components/forecast.component.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ export function ForecastComponent({ forecast, unit }: ForecastProps) {
1515
<motion.div
1616
initial={{ y: 20, opacity: 0 }}
1717
animate={{ y: 0, opacity: 1 }}
18-
className="flex flex-row items-center justify-around h-20 gap-2 p-3 transition-all duration-300 shadow-md bg-gradient-to-b bg-neutral-900/70 backdrop-blur-sm rounded-xl hover:shadow-lg"
18+
className="flex flex-col items-center justify-between h-36 w-24 p-2.5 transition-all duration-300 shadow-md bg-gradient-to-b from-neutral-900/70 to-neutral-800/70 backdrop-blur-sm rounded-xl hover:shadow-lg hover:from-neutral-800/70 hover:to-neutral-700/70 border border-white/5"
1919
>
2020
{/* Time Section */}
21-
<div className="flex flex-col items-center gap-1">
21+
<div className="flex flex-col items-center gap-0.5 w-full">
2222
<div className="text-xs font-semibold tracking-wide uppercase text-neutral-400">
2323
{new Date(forecast.date).toLocaleDateString('fa-IR', {
2424
weekday: 'short',
2525
})}
2626
</div>
27-
<div className="px-3 py-1 text-sm font-medium rounded-full text-neutral-300 bg-neutral-700/30">
27+
<div className="px-2 py-0.5 text-xs font-medium rounded-full text-neutral-300 w-full text-center">
2828
{new Date(forecast.date).toLocaleTimeString([], {
2929
hour: '2-digit',
3030
minute: '2-digit',
@@ -34,17 +34,16 @@ export function ForecastComponent({ forecast, unit }: ForecastProps) {
3434
</div>
3535

3636
{/* Weather Icon */}
37-
<div className="relative group">
37+
<div className="relative my-1 group">
3838
<motion.div
3939
initial={{ rotate: 0 }}
4040
whileHover={{ rotate: [0, -15, 15, 0] }}
4141
transition={{ duration: 0.6 }}
42-
className="p-2"
4342
>
4443
<motion.img
4544
src={forecast.icon}
4645
alt="weather status"
47-
className="w-14 h-14 drop-shadow-weatherIcon"
46+
className="w-12 h-12 drop-shadow-weatherIcon"
4847
initial={{ scale: 0.8 }}
4948
animate={{ scale: 1 }}
5049
transition={{ duration: 0.3 }}
@@ -57,7 +56,7 @@ export function ForecastComponent({ forecast, unit }: ForecastProps) {
5756
<motion.div
5857
initial={{ scale: 0.9 }}
5958
animate={{ scale: 1 }}
60-
className="text-2xl font-extrabold text-transparent bg-gradient-to-r from-gray-400 to-gray-500 bg-clip-text drop-shadow-temperature"
59+
className="text-2xl font-extrabold text-transparent bg-gradient-to-r from-gray-200 to-gray-400 bg-clip-text drop-shadow-temperature"
6160
>
6261
{Math.round(forecast.temp)}
6362
<span className="text-lg font-medium">{unitsFlag[unit]}</span>
Lines changed: 70 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
1-
import { AnimatePresence, motion } from 'motion/react'
2-
import { useEffect, useRef, useState } from 'react'
1+
import { motion } from 'motion/react'
2+
import { useEffect, useState } from 'react'
3+
import { Swiper, SwiperSlide } from 'swiper/react'
34
import { getFromStorage, setToStorage } from '../../common/storage'
4-
import { useGetWeatherByLatLon } from '../../services/getMethodHooks/weather/getWeatherByLatLon'
5-
import type { FetchedWeather } from '../../services/getMethodHooks/weather/weather.interface'
6-
7-
import { Colors } from '../../common/constant/colors.constant'
85
import { useWeatherStore } from '../../context/weather.context'
96
import { useGetForecastWeatherByLatLon } from '../../services/getMethodHooks/weather/getForecastWeatherByLatLon'
7+
import { useGetWeatherByLatLon } from '../../services/getMethodHooks/weather/getWeatherByLatLon'
8+
import type { FetchedWeather } from '../../services/getMethodHooks/weather/weather.interface'
109
import { CurrentWeatherBox } from './components/current-box.component'
1110
import { ForecastComponent } from './components/forecast.component'
11+
//@ts-ignore
12+
import 'swiper/css'
13+
//@ts-ignore
14+
import 'swiper/css/pagination'
15+
//@ts-ignore
16+
import 'swiper/css/navigation'
17+
import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'
18+
import { FreeMode, Navigation, Pagination } from 'swiper/modules'
1219

1320
export function WeatherLayout() {
1421
const { selectedCity, weatherSettings } = useWeatherStore()
1522
const [cityWeather, setCityWeather] = useState<FetchedWeather | null>(null)
1623
const [forecast, setForecast] = useState<FetchedWeather['forecast'] | null>([])
17-
const [isExpanded, setIsExpanded] = useState(false)
18-
const contentRef = useRef<HTMLDivElement>(null)
19-
20-
// Track content height for animation
21-
const [contentHeight, setContentHeight] = useState(210)
24+
if (selectedCity === null) return null
2225

2326
const { data, dataUpdatedAt } = useGetWeatherByLatLon(
2427
selectedCity.lat,
@@ -60,108 +63,86 @@ export function WeatherLayout() {
6063
}
6164
}, [dataUpdatedAt])
6265

63-
// Measure content height when content or expanded state changes
64-
useEffect(() => {
65-
if (contentRef.current && isExpanded) {
66-
setContentHeight(contentRef.current.scrollHeight)
67-
}
68-
}, [forecast, isExpanded])
69-
70-
const visibleItems = isExpanded ? forecast : forecast?.slice(0, 4)
71-
const hasMoreItems = forecast && forecast.length > 4
72-
73-
// Toggle with smooth animation
74-
const toggleExpand = () => {
75-
if (!isExpanded && contentRef.current) {
76-
// Before expanding, measure content height
77-
setContentHeight(contentRef.current.scrollHeight)
78-
}
79-
setIsExpanded((prev) => !prev)
80-
}
81-
8266
return (
8367
<>
8468
<section className="rounded">
85-
<div className="flex flex-col gap-1">
69+
<div className="flex flex-col gap-2">
8670
{cityWeather ? <CurrentWeatherBox weather={cityWeather.weather} /> : null}
8771

8872
<motion.div
89-
ref={contentRef}
90-
className="relative overflow-hidden rounded-lg "
91-
animate={{
92-
height: isExpanded ? contentHeight : 210,
93-
opacity: 1,
94-
}}
73+
className="relative p-1 mt-2 overflow-hidden"
9574
initial={{ opacity: 0.9 }}
96-
transition={{
97-
height: {
98-
duration: 0.5,
99-
ease: [0.04, 0.62, 0.23, 0.98], // Custom smooth easing
100-
},
101-
opacity: { duration: 0.3 },
102-
}}
75+
animate={{ opacity: 1 }}
76+
transition={{ duration: 0.3 }}
10377
>
104-
<div className="grid grid-cols-2 grid-rows-2 gap-2 p-1 overflow-visible">
105-
<AnimatePresence>
106-
{visibleItems?.map((item, index) => (
78+
<Swiper
79+
modules={[Pagination, Navigation, FreeMode]}
80+
spaceBetween={8}
81+
slidesPerView="auto"
82+
freeMode={true}
83+
pagination={false}
84+
navigation={{
85+
nextEl: '.swiper-button-next-custom',
86+
prevEl: '.swiper-button-prev-custom',
87+
}}
88+
className="py-2 weather-forecast-slider"
89+
dir="ltr"
90+
>
91+
{forecast?.map((item, index) => (
92+
<SwiperSlide key={`${item.date}-${index}`} className="w-auto">
10793
<motion.div
108-
key={`${item.date}-${index}`}
109-
initial={index >= 4 ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }}
94+
initial={{ opacity: 0, y: 10 }}
11095
animate={{ opacity: 1, y: 0 }}
111-
exit={index >= 4 ? { opacity: 0, y: -10 } : {}}
11296
transition={{
11397
duration: 0.3,
114-
delay: index >= 4 ? index * 0.05 : 0,
98+
delay: index * 0.05,
11599
}}
116100
>
117101
<ForecastComponent
118102
forecast={item}
119103
unit={weatherSettings.temperatureUnit}
120104
/>
121105
</motion.div>
122-
))}
123-
</AnimatePresence>
124-
</div>
106+
</SwiperSlide>
107+
))}
125108

126-
{hasMoreItems && (
127-
<div
128-
className={`absolute bottom-0 left-0 right-0 flex items-center justify-center ${Colors.bgDiv}`}
129-
>
130-
<motion.button
131-
onClick={toggleExpand}
132-
className="flex items-center justify-center w-full py-1 transition-colors backdrop-blur-sm hover:bg-gray-700/80"
133-
whileTap={{ scale: 0.95 }}
134-
>
135-
<motion.div
136-
animate={{ rotate: isExpanded ? 180 : 0 }}
137-
transition={{
138-
duration: 0.5,
139-
delay: isExpanded ? 0 : 0.1,
140-
ease: 'anticipate',
141-
}}
142-
>
143-
<svg
144-
xmlns="http://www.w3.org/2000/svg"
145-
className="w-5 h-5 text-blue-300"
146-
viewBox="0 0 20 20"
147-
fill="currentColor"
148-
>
149-
<path
150-
fillRule="evenodd"
151-
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
152-
clipRule="evenodd"
153-
/>
154-
</svg>
155-
</motion.div>
156-
<span className="sr-only">
157-
{isExpanded ? 'نمایش کمتر' : 'نمایش بیشتر'}
158-
</span>
159-
</motion.button>
109+
<div className="absolute left-0 z-10 flex items-center justify-center w-8 h-8 text-white transition-all -translate-y-1/2 rounded-full shadow-lg cursor-pointer swiper-button-prev-custom top-1/2 bg-gray-800/80 backdrop-blur-lg hover:bg-gray-700/90">
110+
<FiChevronLeft size={20} />
160111
</div>
161-
)}
112+
113+
<div className="absolute right-0 z-10 flex items-center justify-center w-8 h-8 text-white transition-all -translate-y-1/2 rounded-full shadow-lg cursor-pointer swiper-button-next-custom top-1/2 bg-gray-800/80 backdrop-blur-lg hover:bg-gray-700/90">
114+
<FiChevronRight size={20} />
115+
</div>
116+
</Swiper>
162117
</motion.div>
163118
</div>
164119
</section>
120+
121+
<style>{`
122+
.weather-forecast-slider {
123+
padding-right: 12px !important;
124+
padding-left: 12px !important;
125+
padding-bottom: 5px !important;
126+
}
127+
.weather-forecast-slider .swiper-slide {
128+
width: auto;
129+
height: auto;
130+
}
131+
.swiper-button-prev-custom, .swiper-button-next-custom {
132+
opacity: 0;
133+
transform: translateY(-50%) scale(0.8);
134+
transition: all 0.2s ease;
135+
}
136+
.weather-forecast-slider:hover .swiper-button-prev-custom,
137+
.weather-forecast-slider:hover .swiper-button-next-custom {
138+
opacity: 1;
139+
transform: translateY(-50%) scale(1);
140+
}
141+
.swiper-button-disabled {
142+
opacity: 0.3 !important;
143+
cursor: not-allowed;
144+
}
145+
`}</style>
165146
</>
166147
)
167148
}

0 commit comments

Comments
 (0)