diff --git a/package.json b/package.json index ddeeb3791..5b922c701 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.5+274", + "version": "0.8.5+275", "private": false, "scripts": { "dev": "next dev", diff --git a/src/components/date_picker/date_picker.tsx b/src/components/date_picker/date_picker.tsx index 776524da2..435d8af20 100644 --- a/src/components/date_picker/date_picker.tsx +++ b/src/components/date_picker/date_picker.tsx @@ -168,7 +168,7 @@ const PopulateDates = ({ key={el?.date || `${Date.now()}-${index}`} type="button" disabled={el?.disable ?? true} // Info: (20241108 - Julian) 禁用範圍外和空白日期 - className={`relative z-10 flex h-35px items-center justify-center whitespace-nowrap px-1 text-base transition-all duration-150 ease-in-out disabled:text-date-picker-text-disable md:h-35px ${isSelectedDateStyle} ${isSelectedPeriodStyle} ${!el?.disable ? 'hover:bg-date-picker-surface-date-period' : ''} hover:rounded-full`} + className={`relative z-10 flex h-42px items-center justify-center whitespace-nowrap px-1 text-base transition-all duration-150 ease-in-out disabled:text-date-picker-text-disable ${isSelectedDateStyle} ${isSelectedPeriodStyle} ${!el?.disable ? 'hover:bg-date-picker-surface-date-period' : ''} hover:rounded-full`} onClick={dateClickHandler} > {el?.date ?? ' '} diff --git a/src/components/landing_page_v2/cta.tsx b/src/components/landing_page_v2/cta.tsx index c88326505..ed896727e 100644 --- a/src/components/landing_page_v2/cta.tsx +++ b/src/components/landing_page_v2/cta.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { useTranslation } from 'next-i18next'; @@ -70,6 +70,18 @@ const CTAIntroCard: React.FC<{ const CTA: React.FC = () => { const { t } = useTranslation('common'); + const ctaRef = useRef(null); + const [isCtaRefVisible, setIsCtaRefVisible] = useState(false); + + useEffect(() => { + const waitForCTA = setTimeout(() => { + setIsCtaRefVisible(true); + }, 500); + return () => { + clearTimeout(waitForCTA); + }; + }, []); + // Info: (20241218 - Julian) 卡片內容 const introCards = [ { @@ -103,22 +115,37 @@ const CTA: React.FC = () => { )); return ( -
+
{/* Info: (20241211 - Julian) CTA Main */}
{/* Info: (20241205 - Julian) CTA Main */}
- + {t('landing_page_v2:CTA.MAIN_TITLE')} -

+

{t('landing_page_v2:CTA.MAIN_DESCRIPTION')}

- +

{t('landing_page_v2:CTA.FREE_TRIAL_BTN')}

@@ -127,7 +154,11 @@ const CTA: React.FC = () => {
{/* Info: (20241211 - Julian) CTA Intro Card */} -
+
{displayIntroCards}
diff --git a/src/components/landing_page_v2/easy_to_use.tsx b/src/components/landing_page_v2/easy_to_use.tsx index 760720686..ac6283dff 100644 --- a/src/components/landing_page_v2/easy_to_use.tsx +++ b/src/components/landing_page_v2/easy_to_use.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'next-i18next'; import Image from 'next/image'; import { @@ -110,8 +110,29 @@ const EasyToUse: React.FC = () => { }, ]; + const easyRef = useRef(null); + // Info: (20241218 - Julian) 卡片順序 const [currentOrder, setCurrentOrder] = useState([0, 1, 2]); + // Info: (20250108 - Julian) 播放動畫 + const [isEasyRefVisible, setIsEasyRefVisible] = useState(false); + + const scrollHandler = () => { + if (easyRef.current) { + const rect = (easyRef.current as HTMLElement).getBoundingClientRect(); + const rectTop = rect.top; + const windowHeight = window.innerHeight; + + setIsEasyRefVisible(rectTop < windowHeight); + } + }; + + useEffect(() => { + window.addEventListener('scroll', scrollHandler, { passive: true }); + return () => { + window.removeEventListener('scroll', scrollHandler); + }; + }, []); const toLeft = () => { // Info: (20241218 - Julian) 左移:將最後一個元素移到第一個 @@ -147,14 +168,22 @@ const EasyToUse: React.FC = () => { }); return ( -
+
{/* Info: (20241218 - Julian) Title */} - + {t('landing_page_v2:EASY_TO_USE.MAIN_TITLE')} {/* Info: (20241218 - Julian) Carousel */} -
+
{displayedCards}
diff --git a/src/components/landing_page_v2/financial_report.tsx b/src/components/landing_page_v2/financial_report.tsx index a7d45422e..8dac7fcb8 100644 --- a/src/components/landing_page_v2/financial_report.tsx +++ b/src/components/landing_page_v2/financial_report.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; import { useTranslation } from 'next-i18next'; import { @@ -10,16 +10,49 @@ import { const FinancialReport: React.FC = () => { const { t } = useTranslation('common'); + const reportRef = useRef(null); + const [isReportRefVisible, setIsReportRefVisible] = useState(false); + + const scrollHandler = () => { + if (reportRef.current) { + const rect = (reportRef.current as HTMLElement).getBoundingClientRect(); + const rectTop = rect.top; + const windowHeight = window.innerHeight; + + setIsReportRefVisible(rectTop < windowHeight); + } + }; + + useEffect(() => { + window.addEventListener('scroll', scrollHandler, { passive: true }); + return () => { + window.removeEventListener('scroll', scrollHandler); + }; + }, []); + return ( -
+
{/* Info: (20241218 - Julian) Title */} - + {t('landing_page_v2:REAL_TIME_REPORT.MAIN_TITLE')} {/* Info: (20241218 - Julian) Reports */} -
    +
    • {t('landing_page_v2:REAL_TIME_REPORT.BS')}
    • {t('landing_page_v2:REAL_TIME_REPORT.CFS')}
    • {t('landing_page_v2:REAL_TIME_REPORT.CIS')}
    • @@ -28,7 +61,11 @@ const FinancialReport: React.FC = () => {
{/* Info: (20241218 - Julian) Image */} -
+
{ const { t } = useTranslation('common'); + const featureHeadRef = useRef(null); + const featureFirstRef = useRef(null); + + const [isFeatureHeadRefVisible, setIsFeatureHeadRefVisible] = useState(false); + const [isFeatureFirstRefVisible, setIsFeatureFirstRefVisible] = useState(false); + + const scrollHandler = () => { + if (featureHeadRef.current) { + const rect = (featureHeadRef.current as HTMLElement).getBoundingClientRect(); + const rectTop = rect.top; + const windowHeight = window.innerHeight; + + setIsFeatureHeadRefVisible(rectTop < windowHeight); + } + + if (featureFirstRef.current) { + const rect = (featureFirstRef.current as HTMLElement).getBoundingClientRect(); + const rectTop = rect.top; + const windowHeight = window.innerHeight; + + setIsFeatureFirstRefVisible(rectTop < windowHeight); + } + }; + + useEffect(() => { + window.addEventListener('scroll', scrollHandler, { passive: true }); + return () => { + window.removeEventListener('scroll', scrollHandler); + }; + }, []); + // Info: (20241219 - Julian) 第一分類:主要功能 const featuresOfFirstPart = [ 'Dashboard', @@ -81,14 +112,28 @@ const FlexibleFeatureSelection: React.FC = () => { // const featuresOfFifthPart = ['Manufacturing Management', 'Supply Chain Management']; return ( -
+
{/* Info: (20241219 - Julian) Title */} - + {t('landing_page_v2:FLEXIBLE_FEATURE_SELECTION.MAIN_TITLE')} {/* Info: (20241219 - Julian) Features of First Part */} -
+
{featuresOfFirstPart.map((feature) => ( ))} diff --git a/src/components/landing_page_v2/global_map.tsx b/src/components/landing_page_v2/global_map.tsx index 1d3e34015..3410b8e07 100644 --- a/src/components/landing_page_v2/global_map.tsx +++ b/src/components/landing_page_v2/global_map.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; import { useTranslation } from 'next-i18next'; import { @@ -10,18 +10,52 @@ import { const GlobalMap: React.FC = () => { const { t } = useTranslation('common'); + const mapRef = useRef(null); + const [isMapRefVisible, setIsMapRefVisible] = useState(false); + + const scrollHandler = () => { + if (mapRef.current) { + const rect = (mapRef.current as HTMLElement).getBoundingClientRect(); + const rectTop = rect.top; + const windowHeight = window.innerHeight; + + setIsMapRefVisible(rectTop < windowHeight); + } + }; + + useEffect(() => { + window.addEventListener('scroll', scrollHandler, { passive: true }); + return () => { + window.removeEventListener('scroll', scrollHandler); + }; + }, []); + return ( -
+
- + {t('landing_page_v2:GLOBAL_MAP.MAIN_TITLE')} -

+

{t('landing_page_v2:GLOBAL_MAP.MAIN_DESCRIPTION')}

-
+
map

{t('landing_page_v2:GLOBAL_MAP.COMING_SOON')} diff --git a/src/components/landing_page_v2/happy_customer.tsx b/src/components/landing_page_v2/happy_customer.tsx index 5e76b5494..1988b216e 100644 --- a/src/components/landing_page_v2/happy_customer.tsx +++ b/src/components/landing_page_v2/happy_customer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; import { useTranslation } from 'next-i18next'; import { @@ -24,10 +24,34 @@ const MessageBubble: React.FC<{ const HappyCustomer: React.FC = () => { const { t } = useTranslation('common'); + const customerRef = useRef(null); + const [isCustomerRefVisible, setIsCustomerRefVisible] = useState(false); + + const scrollHandler = () => { + if (customerRef.current) { + const rect = (customerRef.current as HTMLElement).getBoundingClientRect(); + const rectTop = rect.top; + const windowHeight = window.innerHeight; + + setIsCustomerRefVisible(rectTop < windowHeight); + } + }; + + useEffect(() => { + window.addEventListener('scroll', scrollHandler, { passive: true }); + return () => { + window.removeEventListener('scroll', scrollHandler); + }; + }, []); + return (

{/* Info: (20241205 - Julian) Title */} -
+

iSunFA

{t('landing_page_v2:HAPPY_CUSTOMER.MAIN_TITLE')} @@ -35,8 +59,15 @@ const HappyCustomer: React.FC = () => {
{/* Info: (20241205 - Julian) Message Bubbles */} -
- +
+ {/* Info: (20241226 - Julian) Avatar */}
avatar @@ -44,7 +75,11 @@ const HappyCustomer: React.FC = () => { {/* Info: (20241226 - Julian) Message */}

{t('landing_page_v2:HAPPY_CUSTOMER.MESSAGE_1')}

- + {/* Info: (20241226 - Julian) Avatar */}
avatar @@ -52,7 +87,11 @@ const HappyCustomer: React.FC = () => { {/* Info: (20241226 - Julian) Message */}

{t('landing_page_v2:HAPPY_CUSTOMER.MESSAGE_2')}

- + {/* Info: (20241226 - Julian) Avatar */}
avatar @@ -60,7 +99,11 @@ const HappyCustomer: React.FC = () => { {/* Info: (20241226 - Julian) Message */}

{t('landing_page_v2:HAPPY_CUSTOMER.MESSAGE_6')}

- + {/* Info: (20241226 - Julian) Avatar */}
avatar @@ -68,7 +111,11 @@ const HappyCustomer: React.FC = () => { {/* Info: (20241226 - Julian) Message */}

{t('landing_page_v2:HAPPY_CUSTOMER.MESSAGE_3')}

- + {/* Info: (20241226 - Julian) Avatar */}
avatar @@ -76,7 +123,11 @@ const HappyCustomer: React.FC = () => { {/* Info: (20241226 - Julian) Message */}

{t('landing_page_v2:HAPPY_CUSTOMER.MESSAGE_4')}

- + {/* Info: (20241226 - Julian) Avatar */}
avatar @@ -84,7 +135,11 @@ const HappyCustomer: React.FC = () => { {/* Info: (20241226 - Julian) Message */}

{t('landing_page_v2:HAPPY_CUSTOMER.MESSAGE_5')}

- + {/* Info: (20241226 - Julian) Avatar */}
avatar @@ -92,7 +147,11 @@ const HappyCustomer: React.FC = () => { {/* Info: (20241226 - Julian) Message */}

{t('landing_page_v2:HAPPY_CUSTOMER.MESSAGE_7')}

- + {/* Info: (20241226 - Julian) Avatar */}
avatar @@ -100,7 +159,11 @@ const HappyCustomer: React.FC = () => { {/* Info: (20241226 - Julian) Message */}

{t('landing_page_v2:HAPPY_CUSTOMER.MESSAGE_9')}

- + {/* Info: (20241226 - Julian) Avatar */}
avatar diff --git a/src/components/landing_page_v2/linear_gradient_text.tsx b/src/components/landing_page_v2/linear_gradient_text.tsx index 09dadb2fe..0006c3fd4 100644 --- a/src/components/landing_page_v2/linear_gradient_text.tsx +++ b/src/components/landing_page_v2/linear_gradient_text.tsx @@ -1,4 +1,6 @@ -import React from 'react'; +import React, { HTMLAttributes, forwardRef } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils/common'; export enum LinearTextSize { XL = 'xl', @@ -13,36 +15,38 @@ export enum TextAlign { RIGHT = 'right', } -interface ILinearGradientTextProps { - size: LinearTextSize; - align: TextAlign; - children: React.ReactNode; -} +const textVariants = cva( + 'bg-gradient-to-b from-landing-page-white via-landing-page-taupe to-landing-page-taupe2 bg-clip-text font-dm-sans text-transparent', + { + variants: { + size: { + [LinearTextSize.XL]: 'text-2xl font-black md:text-48px lg:text-80px', + [LinearTextSize.LG]: 'text-2xl font-bold md:text-48px lg:text-60px', + [LinearTextSize.MD]: 'text-36px font-bold md:text-44px', + [LinearTextSize.SM]: 'text-lg font-bold md:text-2xl', + }, + align: { + [TextAlign.LEFT]: 'text-left', + [TextAlign.CENTER]: 'text-center', + [TextAlign.RIGHT]: 'text-right', + }, + }, + defaultVariants: { + size: LinearTextSize.LG, + align: TextAlign.LEFT, + }, + } +); -export const LinearGradientText: React.FC = ({ - size, - align, - children, -}) => { - const fontStyle = - size === LinearTextSize.XL - ? 'text-28px font-black md:text-48px lg:text-80px' - : size === LinearTextSize.LG - ? 'text-2xl font-bold md:text-48px lg:text-60px' - : size === LinearTextSize.MD - ? 'text-36px font-bold md:text-44px' - : 'text-lg font-bold md:text-28px'; +interface TextProps extends HTMLAttributes, VariantProps {} - const textAlign = - align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : 'text-left'; +const LinearGradientText = forwardRef( + ({ size, align, className, ...props }, ref) => { + const Comp = 'div'; - return ( -
- {children} -
- ); -}; + return ; + } +); +LinearGradientText.displayName = 'LinearGradientText'; -export default LinearGradientText; +export { LinearGradientText, textVariants }; diff --git a/src/components/landing_page_v2/technical_carousel.tsx b/src/components/landing_page_v2/technical_carousel.tsx index e5070fb5e..93c70d66e 100644 --- a/src/components/landing_page_v2/technical_carousel.tsx +++ b/src/components/landing_page_v2/technical_carousel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; import { useTranslation } from 'next-i18next'; import { FaArrowLeft, FaArrowRight } from 'react-icons/fa6'; @@ -12,6 +12,8 @@ import { LandingButton } from '@/components/landing_page_v2/landing_button'; const TechnicalCarousel: React.FC = () => { const { t } = useTranslation('common'); + const carouselRef = useRef(null); + const carouselData = [ { title: t('landing_page_v2:TECHNICAL_FEATURES.TECHNICAL_PATENTS'), @@ -39,11 +41,30 @@ const TechnicalCarousel: React.FC = () => { const [currentIndex, setCurrentIndex] = useState(0); const [isPaused, setIsPaused] = useState(false); + const [isCarouselRefVisible, setIsCarouselRefVisible] = useState(false); // Info: (20241224 - Julian) 往後翻,如果是第一項則跳到最後一項 const handleLeftClick = () => setCurrentIndex((prev) => (prev === 0 ? lastIdx : prev - 1)); // Info: (20241224 - Julian) 往前翻,如果是最後一項則跳到第一項 const handleRightClick = () => setCurrentIndex((prev) => (prev === lastIdx ? 0 : prev + 1)); + // Info: (20250108 - Julian) 滾動事件綁定動畫 + const scrollHandler = () => { + if (carouselRef.current) { + const rect = (carouselRef.current as HTMLElement).getBoundingClientRect(); + const rectTop = rect.top; + const windowHeight = window.innerHeight; + + setIsCarouselRefVisible(rectTop < windowHeight); + } + }; + + // Info: (20250108 - Julian) 播放動畫 + useEffect(() => { + window.addEventListener('scroll', scrollHandler, { passive: true }); + return () => { + window.removeEventListener('scroll', scrollHandler); + }; + }, []); useEffect(() => { if (isPaused) return undefined; @@ -57,14 +78,25 @@ const TechnicalCarousel: React.FC = () => { }, [isPaused]); return ( -
+
{/* Info: (20241224 - Julian) Image */} -
+
medal_icon
{/* Info: (20241224 - Julian) Text */} -
+
{t('landing_page_v2:TECHNICAL_FEATURES.TECHNICAL_PATENTS')} diff --git a/src/components/landing_page_v2/technical_features.tsx b/src/components/landing_page_v2/technical_features.tsx index ed7554a7c..3da6143e8 100644 --- a/src/components/landing_page_v2/technical_features.tsx +++ b/src/components/landing_page_v2/technical_features.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; import { LinearGradientText, @@ -13,6 +13,7 @@ interface ITechnicalCardProps { content: string; imageSrc: string; imageAlt: string; + isShowAnimation: boolean; } const TechnicalCard: React.FC = ({ @@ -21,9 +22,17 @@ const TechnicalCard: React.FC = ({ content, imageSrc, imageAlt, + isShowAnimation, }) => { + // Info: (20250108 - Julian) 第一張卡片(first)由右邊滑入,第二張卡片(even)由下方滑入,第三張卡片(last)由左邊滑入 + const cardAnim = isShowAnimation + ? 'first:translate-x-0 even:translate-y-0 last:translate-x-0 opacity-100' + : 'first:translate-x-full even:translate-y-full last:-translate-x-full opacity-0'; + return ( -
+
{/* Info: (20241223 - Julian) Brilliant square */}
{/* Info: (20241223 - Julian) Spotlight */} @@ -83,6 +92,25 @@ const TechnicalCard: React.FC = ({ const TechnicalFeatures: React.FC = () => { const { t } = useTranslation('common'); + const technicalRef = useRef(null); + const [isTechnicalRefVisible, setIsTechnicalRefVisible] = useState(false); + + const scrollHandler = () => { + if (technicalRef.current) { + const rect = (technicalRef.current as HTMLElement).getBoundingClientRect(); + const rectTop = rect.top; + const windowHeight = window.innerHeight; + + setIsTechnicalRefVisible(rectTop < windowHeight); + } + }; + + useEffect(() => { + window.addEventListener('scroll', scrollHandler, { passive: true }); + return () => { + window.removeEventListener('scroll', scrollHandler); + }; + }, []); const technicalData = [ { @@ -111,30 +139,36 @@ const TechnicalFeatures: React.FC = () => { return (
{/* Info: (20241205 - Julian) Title */} - + {t('landing_page_v2:TECHNICAL_FEATURES.MAIN_TITLE')} {/* Info: (20241205 - Julian) Content */} -
+
{/* Info: (20241223 - Julian) Background */}
{/* Info: (20241223 - Julian) Cards */} - {technicalData.map((data) => ( - - ))} +
+ {technicalData.map((data) => ( + + ))} +
); diff --git a/src/components/voucher/new_voucher_form.tsx b/src/components/voucher/new_voucher_form.tsx index 47eaaf964..613f33cec 100644 --- a/src/components/voucher/new_voucher_form.tsx +++ b/src/components/voucher/new_voucher_form.tsx @@ -631,16 +631,7 @@ const NewVoucherForm: React.FC = ({ selectedData }) => { e.preventDefault(); // Info: (20241007 - Julian) 若任一條件不符,則中斷 function - if (selectedIds.length === 0) { - // Info: (20241230 - Julian) 如果未選擇憑證,則顯示憑證提示,並定位最上方、吐司通知 - toastHandler({ - id: ToastId.FILL_UP_VOUCHER_FORM, - type: ToastType.ERROR, - content: `${t('journal:ADD_NEW_VOUCHER.TOAST_FILL_UP_FORM')}: certificate`, - closeable: true, - }); - document.body.scrollTop = 0; - } else if (date.startTimeStamp === 0 && date.endTimeStamp === 0) { + if (date.startTimeStamp === 0 && date.endTimeStamp === 0) { // Info: (20241007 - Julian) 日期不可為 0:顯示日期提示,並定位到日期欄位、吐司通知 setIsShowDateHint(true); toastHandler({ diff --git a/src/components/voucher/voucher_editing_page_body.tsx b/src/components/voucher/voucher_editing_page_body.tsx index 348c96685..fd1bbbfd8 100644 --- a/src/components/voucher/voucher_editing_page_body.tsx +++ b/src/components/voucher/voucher_editing_page_body.tsx @@ -659,16 +659,7 @@ const VoucherEditingPageBody: React.FC<{ e.preventDefault(); // Info: (20241007 - Julian) 若任一條件不符,則中斷 function - if (selectedIds.length === 0) { - // Info: (20241230 - Julian) 如果未選擇憑證,則顯示憑證提示,並定位最上方、吐司通知 - toastHandler({ - id: ToastId.FILL_UP_VOUCHER_FORM, - type: ToastType.ERROR, - content: `${t('journal:ADD_NEW_VOUCHER.TOAST_FILL_UP_FORM')}: certificate`, - closeable: true, - }); - document.body.scrollTop = 0; - } else if (date.startTimeStamp === 0 && date.endTimeStamp === 0) { + if (date.startTimeStamp === 0 && date.endTimeStamp === 0) { // Info: (20241007 - Julian) 日期不可為 0:顯示日期提示,並定位到日期欄位、吐司通知 setIsShowDateHint(true); toastHandler({