Skip to content

Commit 7ec0fcb

Browse files
authored
Merge pull request #3 from harmonize-tools/training
Training
2 parents 52a1258 + 67b35a8 commit 7ec0fcb

File tree

10 files changed

+296
-87
lines changed

10 files changed

+296
-87
lines changed

app/installation/page.tsx

Lines changed: 31 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '@heroicons/react/24/outline';
88
import Image from 'next/image';
99
import { useState } from 'react';
10+
import CircularButton from '../ui/components/circular-button';
1011

1112
import Data4HealthCoder from './components/Data4HealthCoder';
1213
import Data4HealthNonCoder from './components/Data4HealthNonCoder';
@@ -41,54 +42,7 @@ interface SubButtonConfig {
4142
}
4243

4344

44-
function CircularButton({ icon, label, onClick, isImage = false, isActive = false }: CircularButtonProps) {
45-
const Icon = icon as React.ElementType; // For when icon is a component
46-
return (
47-
<div className="flex flex-col items-center space-y-2 group">
48-
<div
49-
role="button"
50-
tabIndex={0}
51-
onClick={onClick}
52-
onKeyDown={(e) => {
53-
if (e.key === 'Enter' || e.key === ' ') {
54-
e.preventDefault();
55-
onClick();
56-
}
57-
}}
58-
className={`cursor-pointer transform transition-all duration-300 ease-in-out hover:scale-110 hover:shadow-lg rounded-full p-4 w-24 h-24 flex items-center justify-center
59-
${isActive
60-
? 'bg-purple-100 shadow-lg scale-105 rotate-3'
61-
: 'hover:bg-purple-50 hover:rotate-3'}`}
62-
>
63-
{isImage ? (
64-
<Image
65-
src={icon as string}
66-
alt={label}
67-
width={80}
68-
height={80}
69-
className={`text-current transform transition-transform duration-300
70-
${isActive ? '-rotate-3' : 'group-hover:-rotate-3'}`}
71-
/>
72-
) : (
73-
// allow passing a simple string (emoji) as icon
74-
typeof icon === 'string' ? (
75-
<span className={`text-2xl transition-all duration-300 ${isActive ? 'text-purple-700' : 'text-dark-purple group-hover:text-purple-700'}`}>
76-
{icon}
77-
</span>
78-
) : (
79-
<Icon className={`h-12 w-12 transition-all duration-300
80-
${isActive ? 'text-purple-700' : 'text-dark-purple group-hover:text-purple-700'}`}
81-
/>
82-
)
83-
)}
84-
</div>
85-
<span className={`text-sm font-medium transition-all duration-300
86-
${isActive ? 'text-purple-700' : 'group-hover:text-purple-700'}`}>
87-
{label}
88-
</span>
89-
</div>
90-
);
91-
}
45+
// CircularButton is now a shared component in app/ui/components/circular-button.tsx
9246

9347
interface SubButtonsProps {
9448
section: Section;
@@ -184,6 +138,14 @@ function InstallationContent({ section, type }: InstallationContentProps) {
184138
export default function Page() {
185139
const [activeSection, setActiveSection] = useState<Section | null>(null);
186140
const [activeInstallType, setActiveInstallType] = useState<'coder' | 'noncoder' | null>(null);
141+
// Toggle buttons by editing this map in code. Set true to disable, false to enable.
142+
const disabledMap: Record<Section, boolean> = {
143+
data4health: false,
144+
clim4health: false,
145+
land4health: false,
146+
socio4health: false,
147+
cube4health: false,
148+
};
187149

188150
const handleToolkitClick = (section: Section) => {
189151
if (section === activeSection) {
@@ -207,41 +169,27 @@ export default function Page() {
207169
</p>
208170

209171
<div className="mb-8 mt-6 flex flex-wrap justify-center gap-8">
210-
<CircularButton
211-
icon={D4H_LOGO}
212-
label="data4Health"
213-
onClick={() => handleToolkitClick('data4health')}
214-
isImage={true}
215-
isActive={activeSection === 'data4health'}
216-
/>
217-
<CircularButton
218-
icon={C4H_LOGO}
219-
label="clim4health"
220-
onClick={() => handleToolkitClick('clim4health')}
221-
isImage={true}
222-
isActive={activeSection === 'clim4health'}
223-
/>
224-
<CircularButton
225-
icon={L4H_LOGO}
226-
label="land4health"
227-
onClick={() => handleToolkitClick('land4health')}
228-
isImage={true}
229-
isActive={activeSection === 'land4health'}
230-
/>
231-
<CircularButton
232-
icon={S4H_LOGO}
233-
label="socio4health"
234-
onClick={() => handleToolkitClick('socio4health')}
235-
isImage={true}
236-
isActive={activeSection === 'socio4health'}
237-
/>
238-
<CircularButton
239-
icon={CU4H_LOGO}
240-
label="cube4health"
241-
onClick={() => handleToolkitClick('cube4health')}
242-
isImage={true}
243-
isActive={false}
244-
/>
172+
{[
173+
{ key: 'data4health', logo: D4H_LOGO, label: 'data4Health' },
174+
{ key: 'clim4health', logo: C4H_LOGO, label: 'clim4health' },
175+
{ key: 'land4health', logo: L4H_LOGO, label: 'land4health' },
176+
{ key: 'socio4health', logo: S4H_LOGO, label: 'socio4health' },
177+
{ key: 'cube4health', logo: CU4H_LOGO, label: 'cube4health' },
178+
].map((tk) => {
179+
const section = tk.key as Section;
180+
return (
181+
<div key={tk.key} className="flex flex-col items-center">
182+
<CircularButton
183+
icon={tk.logo}
184+
label={tk.label}
185+
onClick={() => handleToolkitClick(section)}
186+
isImage={true}
187+
isActive={activeSection === section}
188+
disabled={disabledMap[section]}
189+
/>
190+
</div>
191+
);
192+
})}
245193
</div>
246194

247195
{activeSection && (

app/trainings/page.tsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import Image from 'next/image';
5+
import { useState } from 'react';
6+
import CircularButton from '../ui/components/circular-button';
7+
import DownloadButton from '../ui/components/download-button';
8+
9+
const D4H_LOGO = '/cards/toolkits/data4health.svg';
10+
const C4H_LOGO = '/cards/toolkits/clim4health.svg';
11+
const L4H_LOGO = '/cards/toolkits/land4health.svg';
12+
const S4H_LOGO = '/cards/toolkits/socio4health.svg';
13+
const CU4H_LOGO = '/cards/toolkits/cube4health.svg';
14+
15+
16+
type Section = 'data4health' | 'clim4health' | 'land4health' | 'socio4health' | 'cube4health';
17+
18+
interface CircularButtonProps {
19+
icon: React.ElementType | string;
20+
label: string;
21+
onClick: () => void;
22+
isImage?: boolean;
23+
isActive?: boolean;
24+
size?: 'sm' | 'md';
25+
}
26+
27+
28+
function SubButtons({ section }: { section: Section }) {
29+
const resources: Record<Section, { pdf?: string; related?: Array<{ label: string; href: string; isFile?: boolean; useButton?: boolean }> }> = {
30+
data4health: {
31+
pdf: '/training/data4health-training.pdf',
32+
related: [
33+
//{ label: 'Quickstart (download)', href: '/cards/toolkits/data4health-quickstart.zip', isFile: true },
34+
//{ label: 'Online docs', href: 'https://example.org/data4health' },
35+
],
36+
},
37+
clim4health: {
38+
pdf: '/training/clim4health-training.pdf',
39+
related: [
40+
{ label: 'Clim4Health training files', href: '/training/clim4health_for_website.zip', isFile: true, useButton: true },
41+
],
42+
},
43+
land4health: {
44+
pdf: '/cards/toolkits/land4health-installation.pdf',
45+
related: [
46+
{ label: 'Land4Health dataset', href: '/cards/toolkits/land4health-dataset.zip', isFile: true },
47+
{ label: 'Documentation', href: 'https://example.org/land4health' },
48+
],
49+
},
50+
socio4health: {
51+
pdf: '/cards/toolkits/socio4health-installation.pdf',
52+
related: [
53+
{ label: 'Socio4Health notes (download)', href: '/cards/toolkits/socio4health-notes.pdf', isFile: true },
54+
{ label: 'Website', href: 'https://example.org/socio4health' },
55+
],
56+
},
57+
cube4health: {
58+
pdf: '/training/cube4health-training.pdf',
59+
related: [
60+
//{ label: 'Cube4Health examples', href: '/cards/toolkits/cube4health-examples.zip', isFile: true },
61+
//{ label: 'Repo', href: 'https://example.org/cube4health' },
62+
],
63+
},
64+
};
65+
66+
const entry = resources[section];
67+
68+
return (
69+
<article className="prose mx-auto mt-8 w-full max-w-4xl">
70+
71+
{entry?.pdf ? (
72+
<div className="mt-6 border rounded overflow-hidden" style={{ height: 720 }}>
73+
{/* The iframe/embed will show the PDF. If the PDF isn't present, users
74+
can use the link below to open/download it. */}
75+
<iframe
76+
src={entry.pdf}
77+
title={`${section} installation guide`}
78+
className="w-full h-full"
79+
style={{ border: 'none' }}
80+
/>
81+
</div>
82+
) : (
83+
<p className="mt-4 text-sm text-gray-500">No PDF guide available for this toolkit.</p>
84+
)}
85+
86+
{entry.related && entry.related.length > 0 && (
87+
<div className="mt-6">
88+
<h3 className="text-lg font-medium">References</h3>
89+
<div className="mt-3 space-y-3">
90+
{entry.related.map((r, i) => (
91+
<div key={i} className="my-0">
92+
{r.useButton && r.isFile ? (
93+
<DownloadButton href={r.href} label={r.label} />
94+
) : (
95+
<a
96+
href={r.href}
97+
target="_blank"
98+
rel="noopener noreferrer"
99+
className="text-purple-700 hover:underline block"
100+
{...(r.isFile ? { download: '' } : {})}
101+
>
102+
{r.label}
103+
</a>
104+
)}
105+
</div>
106+
))}
107+
</div>
108+
</div>
109+
)}
110+
</article>
111+
);
112+
}
113+
114+
115+
export default function Page() {
116+
const [activeSection, setActiveSection] = useState<Section | null>(null);
117+
// Toggle buttons by editing this map in code. Set true to disable, false to enable.
118+
const disabledMap: Record<Section, boolean> = {
119+
data4health: false,
120+
clim4health: true,
121+
land4health: true,
122+
socio4health: true,
123+
cube4health: true,
124+
};
125+
126+
const handleToolkitClick = (section: Section) => {
127+
if (section === activeSection) {
128+
setActiveSection(null);
129+
} else {
130+
setActiveSection(section);
131+
}
132+
};
133+
134+
return (
135+
<div className="container mx-auto px-4">
136+
<h1 className="mb-2 mt-6 text-3xl font-semibold md:mt-0">Trainings</h1>
137+
<p className="text-justify text-l text-gray-500">
138+
This section contains the workshop development support materials.
139+
</p>
140+
141+
<div className="mb-8 mt-6 flex flex-wrap justify-center gap-8">
142+
{[
143+
{ key: 'data4health', logo: D4H_LOGO, label: 'data4Health' },
144+
{ key: 'clim4health', logo: C4H_LOGO, label: 'clim4health' },
145+
{ key: 'land4health', logo: L4H_LOGO, label: 'land4health' },
146+
{ key: 'socio4health', logo: S4H_LOGO, label: 'socio4health' },
147+
{ key: 'cube4health', logo: CU4H_LOGO, label: 'cube4health' },
148+
].map((tk) => {
149+
const section = tk.key as Section;
150+
return (
151+
<div key={tk.key} className="flex flex-col items-center">
152+
<CircularButton
153+
icon={tk.logo}
154+
label={tk.label}
155+
onClick={() => handleToolkitClick(section)}
156+
isImage={true}
157+
isActive={activeSection === section}
158+
disabled={disabledMap[section]}
159+
/>
160+
</div>
161+
);
162+
})}
163+
</div>
164+
165+
{activeSection && (
166+
<SubButtons section={activeSection} />
167+
)}
168+
</div>
169+
);
170+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React from 'react';
2+
import Image from 'next/image';
3+
4+
export interface CircularButtonProps {
5+
icon: React.ElementType | string;
6+
label: string;
7+
onClick: () => void;
8+
isImage?: boolean;
9+
isActive?: boolean;
10+
disabled?: boolean;
11+
size?: 'sm' | 'md';
12+
}
13+
14+
export default function CircularButton({ icon, label, onClick, isImage = false, isActive = false, disabled = false }: CircularButtonProps) {
15+
const Icon = icon as React.ElementType;
16+
// stronger fade when disabled and remove hover effects
17+
const containerClasses = `transform transition-all duration-300 ease-in-out rounded-full p-4 w-24 h-24 flex items-center justify-center ${
18+
disabled ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:scale-110 hover:shadow-lg'
19+
}`;
20+
21+
const activeBg = isActive ? 'bg-purple-100 shadow-lg scale-105 rotate-3' : (disabled ? '' : 'hover:bg-purple-50 hover:rotate-3');
22+
23+
return (
24+
<div className="flex flex-col items-center space-y-2 group">
25+
<div
26+
role="button"
27+
tabIndex={disabled ? -1 : 0}
28+
aria-disabled={disabled}
29+
onClick={() => {
30+
if (!disabled) onClick();
31+
}}
32+
onKeyDown={(e) => {
33+
if (disabled) return;
34+
if (e.key === 'Enter' || e.key === ' ') {
35+
e.preventDefault();
36+
onClick();
37+
}
38+
}}
39+
className={`${containerClasses} ${activeBg}`}
40+
>
41+
{isImage ? (
42+
<Image
43+
src={icon as string}
44+
alt={label}
45+
width={80}
46+
height={80}
47+
className={`text-current transform transition-transform duration-300 ${disabled ? 'filter grayscale' : ''} ${isActive ? '-rotate-3' : (disabled ? '' : 'group-hover:-rotate-3')}`}
48+
/>
49+
) : (
50+
typeof icon === 'string' ? (
51+
<span className={`text-2xl transition-all duration-300 ${isActive ? 'text-purple-700' : 'text-dark-purple'} ${disabled ? 'text-gray-400' : 'group-hover:text-purple-700'}`}>
52+
{icon}
53+
</span>
54+
) : (
55+
<Icon className={`h-12 w-12 transition-all duration-300 ${isActive ? 'text-purple-700' : 'text-dark-purple'} ${disabled ? 'filter grayscale text-gray-400' : 'group-hover:text-purple-700'}`}
56+
/>
57+
)
58+
)}
59+
</div>
60+
<span className={`text-sm font-medium transition-all duration-300 ${isActive ? 'text-purple-700' : (disabled ? 'text-gray-400' : 'group-hover:text-purple-700')}`}>
61+
{label}
62+
</span>
63+
</div>
64+
);
65+
}

0 commit comments

Comments
 (0)