|
1 |
| -import { useState, useEffect, type Dispatch } from "react"; |
2 |
| -import { DemoDisplay } from "./demo-display"; |
| 1 | +import React, { useState, useMemo, useEffect } from "react"; |
| 2 | +import ReactMarkdown from "react-markdown"; |
3 | 3 | import "./styles/intro-splash.css";
|
4 |
| -import type { IntroSplashEntry } from "../../utils/api"; |
| 4 | +import type { IntroSplashEntry, FeaturedItem } from "../../utils/api"; |
5 | 5 |
|
| 6 | +interface WordPair { |
| 7 | + word: string; |
| 8 | + url: string; |
| 9 | +} |
| 10 | + |
| 11 | +interface ImagePair { |
| 12 | + word: string; |
| 13 | + image: { |
| 14 | + id: string; |
| 15 | + }; |
| 16 | +} |
| 17 | + |
| 18 | +// Props interfaces |
6 | 19 | interface IntroSplashProps {
|
7 | 20 | page_data: IntroSplashEntry[];
|
8 | 21 | }
|
9 | 22 |
|
10 |
| -interface IntroTabProps { |
| 23 | +interface LinksColumnProps { |
11 | 24 | title: string;
|
12 |
| - tab_number: number; |
13 |
| - current_tab: number; |
14 |
| - icon_key: string; |
15 |
| - click_handler: (tab: number) => void; |
| 25 | + link_entries: WordPair[]; |
16 | 26 | }
|
17 | 27 |
|
18 |
| -const IntroTab = ({ |
19 |
| - title, |
20 |
| - tab_number, |
21 |
| - icon_key, |
22 |
| - current_tab, |
23 |
| - click_handler, |
24 |
| -}: IntroTabProps) => { |
25 |
| - const backend_url = import.meta.env.PUBLIC_BACKEND_URL; |
| 28 | +interface ImagesColumnProps { |
| 29 | + title: string; |
| 30 | + image_entries: ImagePair[]; |
| 31 | +} |
| 32 | + |
| 33 | +// Constants |
| 34 | +const backend_url = import.meta.env.PUBLIC_BACKEND_URL; |
| 35 | + |
| 36 | +// Helper functions |
| 37 | +const handleLinkClick = (url: string) => { |
| 38 | + const fullUrl = |
| 39 | + url.startsWith("http://") || url.startsWith("https://") |
| 40 | + ? url |
| 41 | + : `https://${url}`; |
| 42 | + |
| 43 | + window.open(fullUrl, "_blank"); |
| 44 | +}; |
| 45 | + |
| 46 | +const createImageURL = (imageId: string) => { |
| 47 | + return `${backend_url}/assets/${imageId}`; |
| 48 | +}; |
| 49 | + |
| 50 | +// Sub-components |
| 51 | +const ImagesColumn = ({ title, image_entries }: ImagesColumnProps) => { |
| 52 | + const images_object = image_entries?.reduce( |
| 53 | + (acc: any, entry: ImagePair) => { |
| 54 | + acc[entry.image.id] = entry.word; |
| 55 | + return acc; |
| 56 | + }, |
| 57 | + {} |
| 58 | + ); |
| 59 | + |
| 60 | + const [currentImageId, setCurrentImageId] = useState<string>( |
| 61 | + image_entries[0]?.image.id |
| 62 | + ); |
| 63 | + |
26 | 64 | return (
|
27 |
| - <div className={`${current_tab === tab_number ? "current-tab" : ""}`}> |
28 |
| - <div |
29 |
| - className={`intro-anim-option `} |
30 |
| - onClick={() => click_handler(tab_number)} |
31 |
| - style={{ |
32 |
| - animationDelay: "1500ms", |
33 |
| - animationTimingFunction: "ease-out", |
34 |
| - animationDuration: "300ms", |
35 |
| - }} |
36 |
| - > |
37 |
| - <div className="intro-anim-text-icon"> |
38 |
| - <div |
39 |
| - className={`intro-anim-icon ${current_tab === tab_number ? "current-tab-icon" : ""}`} |
40 |
| - > |
41 |
| - <img |
42 |
| - src={`${backend_url}/assets/${icon_key}`} |
43 |
| - alt="Visual Icon" |
44 |
| - /> |
45 |
| - </div> |
46 |
| - <span |
47 |
| - className={`intro-anim-text ${current_tab === tab_number ? "current-tab-text" : ""}`} |
48 |
| - > |
49 |
| - {title} |
50 |
| - </span> |
| 65 | + <div className="image-column-container"> |
| 66 | + <img |
| 67 | + className="current-column-image" |
| 68 | + src={createImageURL(currentImageId)} |
| 69 | + alt={images_object[currentImageId]} |
| 70 | + /> |
| 71 | + <div className="column-container"> |
| 72 | + <div className="column-title">{title}</div> |
| 73 | + |
| 74 | + <div className="column-card"> |
| 75 | + {image_entries?.map((pair, index) => ( |
| 76 | + <div key={index} className="column-entry"> |
| 77 | + <a |
| 78 | + className="entry-text" |
| 79 | + onClick={(e) => { |
| 80 | + e.preventDefault(); |
| 81 | + setCurrentImageId(pair.image.id); |
| 82 | + }} |
| 83 | + > |
| 84 | + {pair.word} |
| 85 | + </a> |
| 86 | + </div> |
| 87 | + ))} |
| 88 | + <div className="column-footer">and many more...</div> |
51 | 89 | </div>
|
52 | 90 | </div>
|
53 | 91 | </div>
|
54 | 92 | );
|
55 | 93 | };
|
56 | 94 |
|
57 |
| -const IntroAnimation = ({ page_data }: IntroSplashProps) => { |
58 |
| - const [currentTab, setCurrentTab] = useState(0); |
| 95 | +const LinksColumn = ({ title, link_entries }: LinksColumnProps) => { |
| 96 | + return ( |
| 97 | + <div className="column-container"> |
| 98 | + <div className="column-title">{title}</div> |
| 99 | + |
| 100 | + <div className="column-card"> |
| 101 | + {link_entries?.map((entry, index) => ( |
| 102 | + <div key={index} className="column-entry"> |
| 103 | + <a |
| 104 | + href={entry.url} |
| 105 | + target="_blank" |
| 106 | + className="entry-text" |
| 107 | + onClick={(e) => { |
| 108 | + e.preventDefault(); |
| 109 | + handleLinkClick(entry.url); |
| 110 | + }} |
| 111 | + > |
| 112 | + {entry.word} |
| 113 | + </a> |
| 114 | + </div> |
| 115 | + ))} |
| 116 | + <div className="column-footer">and many more...</div> |
| 117 | + </div> |
| 118 | + </div> |
| 119 | + ); |
| 120 | +}; |
| 121 | + |
| 122 | +// Main component |
| 123 | +const IntroSplash = ({ page_data }: IntroSplashProps) => { |
| 124 | + // State |
| 125 | + const [introData, setIntroData] = useState<IntroSplashEntry[]>(page_data); |
| 126 | + const [currentTab, setCurrentTab] = useState(introData[0]?.title || ""); |
| 127 | + |
| 128 | + // Process learn more links |
| 129 | + useEffect(() => { |
| 130 | + setIntroData((prev) => { |
| 131 | + prev.forEach((item: IntroSplashEntry) => { |
| 132 | + if (item.learn_more_link) { |
| 133 | + const url = item.learn_more_link; |
| 134 | + const fullUrl = |
| 135 | + url.startsWith("http://") || url.startsWith("https://") |
| 136 | + ? url |
| 137 | + : `https://${url}`; |
| 138 | + |
| 139 | + item.description += ` [Learn more →](${fullUrl})`; |
| 140 | + } |
| 141 | + }); |
| 142 | + |
| 143 | + return [...prev]; |
| 144 | + }); |
| 145 | + }, []); |
| 146 | + |
| 147 | + // Memoized values |
| 148 | + const currentTabData = useMemo(() => { |
| 149 | + return introData.find((tab) => tab.title === currentTab); |
| 150 | + }, [currentTab, introData]); |
| 151 | + |
| 152 | + const FeaturedItems: FeaturedItem[] | undefined = |
| 153 | + currentTabData?.featured_items; |
| 154 | + |
| 155 | + // Check if all items are images (type 1) |
| 156 | + const allItemsAreImages = useMemo(() => { |
| 157 | + if (!FeaturedItems || FeaturedItems.length === 0) return false; |
| 158 | + return FeaturedItems.every((item) => Number(item?.type) === 1); |
| 159 | + }, [FeaturedItems]); |
| 160 | + |
| 161 | + // Render functions |
| 162 | + const renderFeaturedItem = (item: FeaturedItem, index: number) => { |
| 163 | + const itemType = Number(item?.type); |
59 | 164 |
|
60 |
| - const handleTabClick = (tab: number) => { |
61 |
| - setCurrentTab(tab); |
| 165 | + switch (itemType) { |
| 166 | + case 1: |
| 167 | + return ( |
| 168 | + <img |
| 169 | + className={`featured-image ${allItemsAreImages ? "uniform-height" : ""}`} |
| 170 | + src={createImageURL(item?.image?.id || "")} |
| 171 | + alt={currentTabData?.title} |
| 172 | + /> |
| 173 | + ); |
| 174 | + case 2: |
| 175 | + return ( |
| 176 | + <LinksColumn |
| 177 | + key={index} |
| 178 | + title={item?.word_column_title || ""} |
| 179 | + link_entries={item?.column_words || []} |
| 180 | + /> |
| 181 | + ); |
| 182 | + case 3: |
| 183 | + return ( |
| 184 | + <ImagesColumn |
| 185 | + key={index} |
| 186 | + title={item?.image_column_title || ""} |
| 187 | + image_entries={item?.column_images || []} |
| 188 | + /> |
| 189 | + ); |
| 190 | + default: |
| 191 | + return null; |
| 192 | + } |
62 | 193 | };
|
63 | 194 |
|
64 | 195 | return (
|
65 |
| - <div className="intro-anim-cont"> |
66 |
| - <div className="intro-anim-options"> |
67 |
| - {page_data.map((entry, index) => ( |
68 |
| - <IntroTab |
69 |
| - key={entry.id} |
70 |
| - title={entry.title} |
71 |
| - tab_number={index} |
72 |
| - icon_key={entry.icon.id} |
73 |
| - current_tab={currentTab} |
74 |
| - click_handler={handleTabClick} |
75 |
| - /> |
76 |
| - ))} |
| 196 | + <div className="splash-section"> |
| 197 | + <div className="splash-content"> |
| 198 | + <div className="category-buttons"> |
| 199 | + {page_data.map((tab, index) => ( |
| 200 | + <button |
| 201 | + key={index} |
| 202 | + className={`category-button ${currentTab === tab.title ? "active" : ""}`} |
| 203 | + onClick={() => setCurrentTab(tab.title)} |
| 204 | + > |
| 205 | + {tab.title} |
| 206 | + </button> |
| 207 | + ))} |
| 208 | + </div> |
| 209 | + |
| 210 | + <div |
| 211 | + className={`featured-item-container ${allItemsAreImages ? "all-images" : ""}`} |
| 212 | + > |
| 213 | + {FeaturedItems?.map((item, index) => ( |
| 214 | + <React.Fragment key={index}> |
| 215 | + {renderFeaturedItem(item, index)} |
| 216 | + </React.Fragment> |
| 217 | + ))} |
| 218 | + </div> |
| 219 | + |
| 220 | + <div className="featured-item-description"> |
| 221 | + <span className="inline-markdown"> |
| 222 | + <ReactMarkdown>{currentTabData?.description || ""}</ReactMarkdown> |
| 223 | + </span> |
| 224 | + </div> |
77 | 225 | </div>
|
78 |
| - <DemoDisplay |
79 |
| - demo={page_data[currentTab]} |
80 |
| - currentTab={currentTab} |
81 |
| - isLastTab={currentTab == page_data.length - 1} |
82 |
| - /> |
83 | 226 | </div>
|
84 | 227 | );
|
85 | 228 | };
|
86 | 229 |
|
87 |
| -export { IntroAnimation }; |
| 230 | +export { IntroSplash }; |
0 commit comments