Skip to content

Commit b562ff4

Browse files
authored
Learn solidity landing page (#320)
1 parent fe9b0bd commit b562ff4

File tree

9 files changed

+515
-10
lines changed

9 files changed

+515
-10
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import Link from "next/link";
2+
import { MDXRemote } from "next-mdx-remote/rsc";
3+
import rehypeRaw from "rehype-raw";
4+
import remarkGfm from "remark-gfm";
5+
import { CheckCircleIcon, ClockIcon } from "@heroicons/react/24/outline";
6+
import SkillLevelIcon from "~~/app/_components/SkillLevelIcon";
7+
import { roadmap } from "~~/app/learn-solidity/_components/data";
8+
import type { CurriculumGroup as Group, RoadmapSection as SectionData } from "~~/app/learn-solidity/_components/data";
9+
import type { ChallengeId } from "~~/services/database/config/types";
10+
import { Challenges } from "~~/services/database/repositories/challenges";
11+
import { CHALLENGE_METADATA } from "~~/utils/challenges";
12+
import type { SkillLevel } from "~~/utils/challenges";
13+
14+
export const CurriculumSection = ({ challenges }: { challenges: Challenges }) => {
15+
const challengeById = Object.fromEntries(challenges.map(c => [c.id, c]));
16+
17+
return (
18+
<section id="curriculum" className="container mx-auto px-6 py-16">
19+
<div className="text-center mb-12">
20+
<h2 className="text-3xl md:text-4xl font-bold mb-4">Your Solidity Tutorial Roadmap</h2>
21+
<p className="text-base-content/80 max-w-3xl mx-auto">
22+
Follow our structured learning path from your first smart contract to advanced Web3 concepts.
23+
</p>
24+
</div>
25+
26+
{(["fundamentals", "advanced"] as Group[]).map((group: Group) => {
27+
const groupLabel = group === "fundamentals" ? "🧱 Solidity Fundamentals" : "⚡ Advanced Solidity Concepts";
28+
const groupDesc =
29+
group === "fundamentals"
30+
? "Master the core concepts of Ethereum development and smart contract basics."
31+
: "Build complex DeFi protocols and advanced smart contract systems";
32+
33+
return (
34+
<div key={group} className="mb-12 rounded-2xl p-4 md:p-6 bg-accent mx-auto lg:max-w-5xl xl:max-w-6xl">
35+
<div className="text-center mb-5">
36+
<h3 className="text-2xl md:text-3xl font-bold text-[#026262]">{groupLabel}</h3>
37+
<p className="text-[#026262]">{groupDesc}</p>
38+
</div>
39+
40+
<div className="space-y-5">
41+
{roadmap
42+
.filter((s: SectionData) => s.group === group)
43+
.map((section: SectionData) => {
44+
return (
45+
<div key={section.title} className="card bg-base-100 shadow-md overflow-hidden">
46+
<div className="card-body p-5">
47+
<div className="flex items-start">
48+
<div>
49+
<h4 className="card-title text-xl mb-1">{section.title}</h4>
50+
<p className="text-sm text-base-content/80">{section.description}</p>
51+
</div>
52+
</div>
53+
54+
<div className="grid md:grid-cols-2 gap-5 mt-4 lg:max-w-none">
55+
<div>
56+
<h5 className="font-semibold mb-2 text-primary">🎯 Challenges</h5>
57+
<ul className="space-y-2">
58+
{section.challengeIds.map((id: ChallengeId) => {
59+
const ch = challengeById[id];
60+
if (!ch) return null;
61+
62+
const meta = CHALLENGE_METADATA[id];
63+
const desc = meta?.description || ch.description;
64+
65+
return (
66+
<li key={id}>
67+
<Link
68+
href={`/challenge/${ch.id}`}
69+
className="block p-3 rounded-lg bg-base-200 hover:bg-base-300 transition-colors"
70+
>
71+
<div className="flex items-center gap-3">
72+
<div className="min-w-0">
73+
<div className="font-medium">{ch.challengeName}</div>
74+
<div className="text-xs text-base-content/70 leading-snug">{desc}</div>
75+
</div>
76+
</div>
77+
</Link>
78+
</li>
79+
);
80+
})}
81+
</ul>
82+
</div>
83+
<div>
84+
<h5 className="font-semibold mb-2 text-primary">📚 Recommended Resources</h5>
85+
<ul className="space-y-1 list-disc pl-5 marker:text-base-content/50">
86+
{(() => {
87+
const guideLinks = section.challengeIds.flatMap(
88+
id => CHALLENGE_METADATA[id]?.guides ?? [],
89+
);
90+
const helpfulLinks = section.challengeIds
91+
.flatMap(id => CHALLENGE_METADATA[id]?.helpfulLinks ?? [])
92+
.filter(
93+
l => !l.url || (!l.url.startsWith("/guides") && !l.url.includes("/challenge/")),
94+
)
95+
.map(l => ({ title: l.text, url: l.url ?? "#" }));
96+
const combined = [...guideLinks, ...helpfulLinks];
97+
const uniqueByUrl = Array.from(
98+
new Map(combined.map(item => [item.url, item])).values(),
99+
);
100+
return uniqueByUrl.map(item => (
101+
<li key={item.url} className="pl-1 text-sm text-primary">
102+
<Link href={item.url} className="hover:underline">
103+
{item.title}
104+
</Link>
105+
</li>
106+
));
107+
})()}
108+
</ul>
109+
</div>
110+
</div>
111+
112+
<div className="mt-4 rounded-lg p-3 md:p-4 bg-base-200">
113+
<div className="flex flex-col lg:flex-row gap-6 items-stretch">
114+
<div className="lg:basis-[60%] p-2">
115+
<h6 className="text-base font-semibold text-primary mb-2">Skills you&apos;ll gain</h6>
116+
<ul className="space-y-2">
117+
{section.challengeIds
118+
.flatMap(id => CHALLENGE_METADATA[id]?.skills ?? [])
119+
.map(s => (
120+
<li key={s} className="flex items-center gap-2 text-sm">
121+
<CheckCircleIcon className="w-5 h-5 text-primary self-start shrink-0" />
122+
<span className="inline">
123+
<MDXRemote
124+
source={s}
125+
options={{
126+
mdxOptions: {
127+
rehypePlugins: [rehypeRaw],
128+
remarkPlugins: [remarkGfm],
129+
format: "md",
130+
},
131+
}}
132+
components={{
133+
p: (props: any) => <p className="m-0 inline">{props.children}</p>,
134+
}}
135+
/>
136+
</span>
137+
</li>
138+
))}
139+
</ul>
140+
</div>
141+
<div className="lg:basis-[40%] grid grid-cols-1 md:grid-cols-2 gap-4 min-w-fit items-center text-sm">
142+
<div className="flex items-center gap-3 text-sm">
143+
<SkillLevelIcon
144+
level={CHALLENGE_METADATA[section.challengeIds[0]]?.skillLevel as SkillLevel}
145+
className="w-6 h-6 text-primary"
146+
/>
147+
<div>
148+
<div className="font-medium">Skill level</div>
149+
<div className="text-base-content/70">
150+
{CHALLENGE_METADATA[section.challengeIds[0]]?.skillLevel || "Beginner"}
151+
</div>
152+
</div>
153+
</div>
154+
<div className="flex items-center gap-3 text-sm">
155+
<ClockIcon className="w-6 h-6 text-primary" />
156+
<div>
157+
<div className="font-medium">Time to complete</div>
158+
<div className="text-base-content/70">
159+
{CHALLENGE_METADATA[section.challengeIds[0]]?.timeToComplete || ""}
160+
</div>
161+
</div>
162+
</div>
163+
</div>
164+
</div>
165+
</div>
166+
</div>
167+
</div>
168+
);
169+
})}
170+
</div>
171+
</div>
172+
);
173+
})}
174+
</section>
175+
);
176+
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import Link from "next/link";
5+
import { useRouter } from "next/navigation";
6+
import { ConnectButton } from "@rainbow-me/rainbowkit";
7+
import { useLocalStorage } from "usehooks-ts";
8+
import { useAccount } from "wagmi";
9+
import { useUser } from "~~/hooks/useUser";
10+
import { useUserRegister } from "~~/hooks/useUserRegister";
11+
12+
export const DynamicCtaButton = () => {
13+
const { address: connectedAddress } = useAccount();
14+
const { data: user, isLoading: isLoadingUser } = useUser(connectedAddress);
15+
const { handleRegister, isRegistering } = useUserRegister();
16+
const [isLocalLoading, setIsLocalLoading] = useState(false);
17+
const [storedReferrer] = useLocalStorage<string | null>("originalReferrer", null);
18+
const [storedUtmParams] = useLocalStorage<string | null>("originalUtmParams", null);
19+
const parsedUtmParams = storedUtmParams ? JSON.parse(storedUtmParams) : undefined;
20+
const router = useRouter();
21+
22+
const isWalletConnected = !!connectedAddress;
23+
const isLoading = isLocalLoading || isRegistering;
24+
25+
const handleRegisterClick = async () => {
26+
setIsLocalLoading(true);
27+
try {
28+
await handleRegister({ referrer: storedReferrer, originalUtmParams: parsedUtmParams });
29+
} catch (error) {
30+
console.error("Registration failed:", error);
31+
} finally {
32+
setIsLocalLoading(false);
33+
router.push(`/builders/${connectedAddress}`);
34+
}
35+
};
36+
37+
if (isLoadingUser) {
38+
return (
39+
<div className="flex justify-center">
40+
<span className="loading loading-spinner loading-md"></span>
41+
</div>
42+
);
43+
}
44+
45+
// If user is registered, show "go to your portfolio" button
46+
if (user && !isLoadingUser) {
47+
return (
48+
<div className="flex justify-center">
49+
<Link href={`/builders/${connectedAddress}`}>
50+
<button
51+
className="w-[300px] h-[60px] text-gray-600 bg-[url('/assets/start/button-frame.svg')] bg-no-repeat bg-center bg-contain flex items-center justify-center hover:opacity-75"
52+
type="button"
53+
>
54+
Go to your Portfolio
55+
</button>
56+
</Link>
57+
</div>
58+
);
59+
}
60+
61+
return (
62+
<div className="flex justify-center">
63+
{!isWalletConnected ? (
64+
<ConnectButton.Custom>
65+
{({ openConnectModal }) => (
66+
<button
67+
className="w-[300px] h-[60px] text-gray-600 bg-[url('/assets/start/button-frame.svg')] bg-no-repeat bg-center bg-contain flex items-center justify-center hover:opacity-75"
68+
onClick={openConnectModal}
69+
type="button"
70+
>
71+
Connect Wallet
72+
</button>
73+
)}
74+
</ConnectButton.Custom>
75+
) : (
76+
<button
77+
className="w-[300px] h-[60px] text-gray-600 bg-[url('/assets/start/button-frame.svg')] bg-no-repeat bg-center bg-contain flex items-center justify-center hover:opacity-75"
78+
onClick={handleRegisterClick}
79+
disabled={isLoading}
80+
>
81+
{isLoading ? <span className="loading loading-spinner loading-md"></span> : <>✍️ Register as Builder</>}
82+
</button>
83+
)}
84+
</div>
85+
);
86+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { faqs } from "./data";
2+
3+
export const FaqSection = () => {
4+
return (
5+
<section id="faq" className="container mx-auto px-6 py-16">
6+
<h2 className="text-3xl md:text-4xl font-bold text-center mb-8">Solidity Course FAQs</h2>
7+
<div className="max-w-3xl mx-auto">
8+
{faqs.map((item, idx) => (
9+
<div key={idx} className="collapse collapse-arrow bg-base-100 mb-3 shadow-sm">
10+
<input type="checkbox" />
11+
<div className="collapse-title text-lg font-medium">{item.q}</div>
12+
<div className="collapse-content">
13+
<p className="text-base-content/80">{item.a}</p>
14+
</div>
15+
</div>
16+
))}
17+
</div>
18+
</section>
19+
);
20+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { DynamicCtaButton } from "./DynamicCtaButton";
2+
3+
export const HeroSection = () => {
4+
return (
5+
<section className="relative bg-base-300">
6+
<div className="absolute inset-0 bg-[url('/assets/home_header_clouds.svg')] bg-top bg-repeat-x bg-[length:auto_200px] sm:bg-[length:auto_300px]" />
7+
<div className="relative container mx-auto px-6 py-16 md:py-24 text-center">
8+
<h1 className="text-4xl md:text-6xl font-bold leading-tight">
9+
Learn Solidity with our
10+
<br className="hidden md:block" />
11+
<span className="text-primary"> Web3 Developer Course</span>
12+
</h1>
13+
<p className="mt-6 max-w-3xl mx-auto text-base-content/80 md:text-lg">
14+
Master Solidity with our hands-on tutorials. Build real dApps, complete interactive challenges, and create
15+
your on-chain developer portfolio.
16+
</p>
17+
<div className="mt-6 flex flex-wrap justify-center gap-6 text-sm text-base-content/70">
18+
<div className="flex items-center gap-2">
19+
<span className="w-2 h-2 bg-primary rounded-full" /> 100% Free
20+
</div>
21+
<div className="flex items-center gap-2">
22+
<span className="w-2 h-2 bg-primary rounded-full" /> Hands‑On Learning
23+
</div>
24+
<div className="flex items-center gap-2">
25+
<span className="w-2 h-2 bg-primary rounded-full" /> Build Real Projects
26+
</div>
27+
</div>
28+
<div className="mt-8">
29+
<DynamicCtaButton />
30+
</div>
31+
</div>
32+
<div className="relative h-[130px]">
33+
<div className="absolute inset-0 bg-[url('/assets/header_platform.svg')] bg-repeat-x bg-[length:auto_130px] z-10" />
34+
<div className="bg-base-200 absolute inset-0 top-auto w-full h-5" />
35+
</div>
36+
</section>
37+
);
38+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { SpaceshipIcon, TargetIcon, ToolsIcon } from "../../start/_components/Icons";
2+
3+
export const WhyWorks = () => {
4+
const items = [
5+
{
6+
title: "Learn by Building",
7+
desc: "Every challenge teaches through hands-on practice. Each delivers a key 'aha' moment.",
8+
Icon: ToolsIcon,
9+
color: "primary" as const,
10+
},
11+
{
12+
title: "Real Portfolio",
13+
desc: "Each completed challenge becomes part of your on-chain, verifiable builder profile.",
14+
Icon: TargetIcon,
15+
color: "accent" as const,
16+
},
17+
{
18+
title: "Production Ready",
19+
desc: "Use Scaffold-ETH 2 to build production-grade dApps.",
20+
Icon: SpaceshipIcon,
21+
color: "secondary" as const,
22+
},
23+
];
24+
25+
return (
26+
<section className="bg-base-300">
27+
<div className="container mx-auto px-6 py-16">
28+
<div className="text-center mb-10">
29+
<h3 className="text-2xl md:text-3xl font-bold">Why SpeedRunEthereum Works</h3>
30+
<p className="text-base-content/70 mt-2">
31+
Our hands-on approach gets you building real projects from day one
32+
</p>
33+
</div>
34+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
35+
{items.map(({ title, desc, Icon }) => {
36+
const iconClasses = "text-accent";
37+
return (
38+
<div key={title} className="card bg-base-100 shadow-sm">
39+
<div className="card-body items-center text-center">
40+
<div className={`mb-3 ${iconClasses}`}>
41+
<Icon />
42+
</div>
43+
<h4 className="card-title justify-center">{title}</h4>
44+
<p className="text-sm text-base-content/80">{desc}</p>
45+
</div>
46+
</div>
47+
);
48+
})}
49+
</div>
50+
</div>
51+
</section>
52+
);
53+
};

0 commit comments

Comments
 (0)