This repository has been archived on 2024-12-27. You can view files and clone it, but cannot push or open issues or pull requests.
ez-web/src/App.tsx
2024-11-02 13:08:08 +08:00

166 lines
5.3 KiB
TypeScript

import React, { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
// 倒计时组件的属性接口
interface CountdownProps {
startDate: string
firstPeriodDays: number
secondPeriodDays: number
extraHours: number
}
// 文章接口
interface Article {
id: number
title: string
content: string
created_at: string
type: 'activity' | 'news'
}
// 倒计时组件
function Countdown({ startDate, firstPeriodDays, secondPeriodDays, extraHours }: CountdownProps) {
const [remainingDays, setRemainingDays] = useState(0)
const [isFirstPeriod, setIsFirstPeriod] = useState(true)
useEffect(() => {
const calculateCountdown = () => {
const totalDays = firstPeriodDays + secondPeriodDays
const currentTime = new Date().getTime()
const elapsedDays = Math.floor((currentTime - new Date(startDate).getTime()) / (1000 * 60 * 60 * 24))
const currentPeriod = elapsedDays % totalDays
if (currentPeriod < firstPeriodDays) {
setIsFirstPeriod(true)
setRemainingDays(firstPeriodDays - currentPeriod)
} else {
setIsFirstPeriod(false)
setRemainingDays(totalDays - currentPeriod)
}
}
calculateCountdown()
const timer = setInterval(calculateCountdown, 1000 * 60 * 60) // 每小时更新一次
return () => clearInterval(timer)
}, [startDate, firstPeriodDays, secondPeriodDays])
return (
<motion.div
className="icon text-center bg-white rounded-lg shadow-lg p-6 w-full sm:w-64"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<p className="text-lg font-semibold mb-2">{isFirstPeriod ? '距离下一次大休还有' : '距离开学还有'}</p>
<motion.a
className={`text-6xl font-bold ${isFirstPeriod ? 'text-blue-500' : 'text-red-500'}`}
initial={{ scale: 1 }}
animate={{ scale: [1, 1.1, 1] }}
transition={{ duration: 1, repeat: Infinity, repeatType: "reverse" }}
>
{remainingDays}
</motion.a>
<p className="text-lg mt-2"></p>
</motion.div>
)
}
// 文章组件
function Articles() {
const [articles, setArticles] = useState<Article[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchArticles = async () => {
try {
const response = await fetch('https://ez-api.mei.lv:55233/public/home')
if (!response.ok) {
throw new Error('Failed to fetch articles')
}
const data = await response.json()
setArticles(data)
setLoading(false)
} catch (err) {
setError('Failed to load articles. Please try again later.')
setLoading(false)
}
}
fetchArticles()
}, [])
const activities = articles.filter(article => article.type === 'activity')
const news = articles.filter(article => article.type === 'news')
const renderArticles = (articles: Article[]) => {
return articles.map((article, index) => (
<motion.div
key={article.id}
className="mb-6 bg-white rounded-lg shadow-md p-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<a href={`/article/${article.id}.html`} className="text-xl font-semibold hover:text-blue-600 transition-colors duration-200">
{article.title}
</a>
<p className="mt-2 text-gray-600 truncate">{article.content}</p>
<small className="text-gray-500">: {article.created_at}</small>
</motion.div>
))
}
if (loading) {
return <div className="text-center">...</div>
}
if (error) {
return <div className="text-center text-red-500">{error}</div>
}
return (
<div className="articles grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="left-column">
<h2 className="text-2xl font-bold mb-4 text-blue-600"></h2>
{renderArticles(activities)}
</div>
<div className="right-column">
<h2 className="text-2xl font-bold mb-4 text-green-600"></h2>
{renderArticles(news)}
</div>
</div>
)
}
// 主应用组件
export default function App() {
return (
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col items-center mb-12">
<div className="icons grid grid-cols-1 sm:grid-cols-2 gap-6 w-full max-w-2xl mb-8">
<Countdown
startDate="2024-10-06T16:25:00"
firstPeriodDays={12}
secondPeriodDays={2}
extraHours={0}
/>
<motion.div
className="icon text-center bg-white rounded-lg shadow-lg p-6 w-full sm:w-64"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<p className="text-lg font-semibold mb-2"></p>
<button className="text-4xl font-bold text-green-500"></button>
</motion.div>
</div>
</div>
<div className="w-full h-px bg-gray-300 my-12"></div>
<Articles />
</div>
</div>
)
}