diff --git a/docusaurus.config.ts b/docusaurus.config.ts index aa7a0da..f8ce085 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -3,9 +3,10 @@ import type {Config} from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; const config: Config = { - title: 'EduIDE Docs', - tagline: 'Documentation and product guidance for EduIDE', + title: 'edu ide — The IDE built for learning', + tagline: 'A browser-based development environment designed for education', favicon: 'img/logo.svg', + future: { v4: true, }, diff --git a/src/components/landing/Button.module.css b/src/components/landing/Button.module.css new file mode 100644 index 0000000..cf72bb9 --- /dev/null +++ b/src/components/landing/Button.module.css @@ -0,0 +1,43 @@ +.btnPrimary { + background: var(--landing-teal); + color: #fff !important; + border: none; + padding: 13px 24px; + border-radius: 10px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + font-family: 'DM Sans', sans-serif; + transition: background 0.15s, transform 0.1s; + text-decoration: none !important; + display: inline-block; +} + +.btnPrimary:hover { + background: var(--landing-teal-dark); + color: #fff !important; +} + +.btnPrimary:active { + transform: scale(0.98); +} + +.btnSecondary { + background: transparent; + color: var(--landing-text) !important; + border: 0.5px solid var(--landing-border-med); + padding: 13px 24px; + border-radius: 10px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + font-family: 'DM Sans', sans-serif; + transition: background 0.15s; + text-decoration: none !important; + display: inline-block; +} + +.btnSecondary:hover { + background: var(--landing-surface); + color: var(--landing-text) !important; +} diff --git a/src/components/landing/Button.tsx b/src/components/landing/Button.tsx new file mode 100644 index 0000000..9305197 --- /dev/null +++ b/src/components/landing/Button.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import styles from './Button.module.css'; + +interface ButtonProps { + variant: 'primary' | 'secondary'; + to: string; + target?: string; + children: React.ReactNode; +} + +const Button = React.memo(function Button({ + variant, + to, + target, + children, +}) { + return ( + + {children} + + ); +}); + +export default Button; diff --git a/src/components/landing/CtaSection.module.css b/src/components/landing/CtaSection.module.css new file mode 100644 index 0000000..cdf243e --- /dev/null +++ b/src/components/landing/CtaSection.module.css @@ -0,0 +1,39 @@ +.ctaSection { + padding: 5rem 2rem; + text-align: center; + max-width: 700px; + margin: 0 auto; +} + +.ctaSection h2 { + font-size: 38px; + font-weight: 400; + margin-bottom: 1rem; + line-height: 1.15; + color: var(--landing-text); +} + +.ctaSection p { + font-size: 16px; + color: var(--landing-muted); + margin-bottom: 2.5rem; + line-height: 1.7; +} + +.ctaActions { + display: flex; + gap: 14px; + justify-content: center; + flex-wrap: wrap; +} + +@media (max-width: 560px) { + .ctaSection h2 { + font-size: 28px; + } + + .ctaActions { + flex-direction: column; + align-items: center; + } +} diff --git a/src/components/landing/CtaSection.tsx b/src/components/landing/CtaSection.tsx new file mode 100644 index 0000000..747b7ab --- /dev/null +++ b/src/components/landing/CtaSection.tsx @@ -0,0 +1,26 @@ +import Button from "./Button"; +import styles from "./CtaSection.module.css"; + +export default function CtaSection() { + return ( +
+

Ready to try EduIDE?

+

+ Open the IDE in your browser. Or deploy it for your next course in + minutes. +

+
+ + +
+
+ ); +} diff --git a/src/components/landing/FeatureShowcase.module.css b/src/components/landing/FeatureShowcase.module.css new file mode 100644 index 0000000..62194ab --- /dev/null +++ b/src/components/landing/FeatureShowcase.module.css @@ -0,0 +1,140 @@ +.section { + padding: 5rem 2rem; + max-width: 1100px; + margin: 0 auto; +} + +.inner { + display: grid; + grid-template-columns: 1fr 1.5fr; + gap: 4rem; + align-items: center; +} + +.inner.reverse { + grid-template-columns: 1.5fr 1fr; +} + +.inner.reverse .textCol { + order: 2; +} + +.inner.reverse .mediaCol { + order: 1; +} + +.textCol { + display: flex; + flex-direction: column; + gap: 0; +} + +.label { + font-size: 12px; + font-weight: 500; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--landing-teal); + margin-bottom: 0.75rem; +} + +.textCol h2 { + font-size: 2rem; + font-weight: 600; + line-height: 1.15; + letter-spacing: -0.03em; + color: var(--landing-text); + margin-bottom: 1rem; +} + +.textCol p { + font-size: 16px; + color: var(--landing-muted); + line-height: 1.75; + margin-bottom: 1.5rem; +} + +.link { + font-size: 15px; + font-weight: 500; + color: var(--landing-teal); + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 5px; +} + +.link:hover { + text-decoration: underline; +} + +.mediaCol { + position: relative; + overflow: hidden; + background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%); + aspect-ratio: 16 / 10; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12); +} + +.mediaCol.hasVideo { + background: none; + aspect-ratio: auto; + border-radius: 0; + box-shadow: none; + mask-image: + linear-gradient(to right, transparent 0%, black 4%, black 96%, transparent 100%), + linear-gradient(to bottom, transparent 0%, black 4%, black 88%, transparent 100%); + mask-composite: intersect; + -webkit-mask-image: + linear-gradient(to right, transparent 0%, black 4%, black 96%, transparent 100%), + linear-gradient(to bottom, transparent 0%, black 4%, black 88%, transparent 100%); + -webkit-mask-composite: source-in; +} + +.mediaCol img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + border-radius: 16px; +} + +.mediaCol video { + display: block; + width: 100%; + height: auto; +} + +.gifPlaceholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + color: rgba(255, 255, 255, 0.7); + font-size: 14px; +} + +.gifPlaceholder svg { + opacity: 0.5; +} + +@media (max-width: 820px) { + .inner, + .inner.reverse { + grid-template-columns: 1fr; + gap: 2rem; + } + + .inner.reverse .textCol { + order: 0; + } + + .inner.reverse .mediaCol { + order: 0; + } +} diff --git a/src/components/landing/FeatureShowcase.tsx b/src/components/landing/FeatureShowcase.tsx new file mode 100644 index 0000000..2702b80 --- /dev/null +++ b/src/components/landing/FeatureShowcase.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import { useScrollReveal } from '../../hooks/useScrollReveal'; +import styles from './FeatureShowcase.module.css'; + +interface FeatureShowcaseProps { + label: string; + title: string; + description: string; + linkText?: string; + linkHref?: string; + mediaSrc?: string; + mediaPoster?: string; + mediaAlt?: string; + mediaType?: 'img' | 'gif' | 'video'; + reverse?: boolean; + accentColor?: string; +} + +function useDeferredSrc(src: string | undefined): string | undefined { + const [ready, setReady] = React.useState(false); + React.useEffect(() => { + if (!src) return; + if (document.readyState === 'complete') { + setReady(true); + return () => {}; + } + const handler = () => setReady(true); + window.addEventListener('load', handler, { once: true }); + return () => window.removeEventListener('load', handler); + }, [src]); + return ready ? src : undefined; +} + +function MediaArea({ + mediaSrc, + mediaPoster, + mediaAlt, + mediaType, + accentColor, +}: Pick) { + const deferredSrc = useDeferredSrc(mediaType === 'video' ? mediaSrc : undefined); + const style = accentColor ? { background: accentColor } : undefined; + + if (mediaSrc) { + const isVideo = mediaType === 'video'; + const className = isVideo + ? `${styles.mediaCol} ${styles.hasVideo}` + : styles.mediaCol; + + return ( +
+ {isVideo ? ( +
+ ); + } + + return ( +
+
+ {/* Codicon: device-desktop (16x16) */} + + + + GIF coming soon +
+
+ ); +} + +const FeatureShowcase = React.memo(function FeatureShowcase({ + label, + title, + description, + linkText, + linkHref, + mediaSrc, + mediaPoster, + mediaAlt, + mediaType = 'img', + reverse = false, + accentColor, +}) { + const sectionRef = useScrollReveal(); + const resolvedMediaSrc = useBaseUrl(mediaSrc ?? ''); + const resolvedMediaPoster = useBaseUrl(mediaPoster ?? ''); + + return ( +
+
+
+
{label}
+

{title}

+

{description}

+ {linkText && linkHref && ( + + {linkText} + + )} +
+ +
+
+ ); +}); + +export default FeatureShowcase; diff --git a/src/components/landing/HeroSection.module.css b/src/components/landing/HeroSection.module.css new file mode 100644 index 0000000..aafd8aa --- /dev/null +++ b/src/components/landing/HeroSection.module.css @@ -0,0 +1,186 @@ +/* ─── Hero layout ────────────────────────────────────────────────────────── */ +.hero { + position: relative; + max-width: 1200px; + margin: 0 auto; + padding: 5rem 2rem 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 3rem; +} + +/* ─── Decorative grid background ─────────────────────────────────────────── */ +.heroGridBg { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 100vw; + height: 100%; + background-image: + linear-gradient(var(--landing-grid-line) 1px, transparent 1px), + linear-gradient(90deg, var(--landing-grid-line) 1px, transparent 1px); + background-size: 44px 44px; + mask-image: linear-gradient(to bottom, transparent, rgba(0,0,0,0.55) 12%, rgba(0,0,0,0.55) 72%, transparent); + -webkit-mask-image: linear-gradient(to bottom, transparent, rgba(0,0,0,0.55) 12%, rgba(0,0,0,0.55) 72%, transparent); + pointer-events: none; + z-index: 0; +} + +/* ─── Teal glow orb ──────────────────────────────────────────────────────── */ +.heroGlow { + position: absolute; + top: -80px; + left: -120px; + width: 680px; + height: 680px; + background: radial-gradient(circle, rgba(78, 159, 156, 0.16) 0%, transparent 65%); + pointer-events: none; + z-index: 0; + border-radius: 50%; +} + +/* ─── Content layers above decorative elements ───────────────────────────── */ +.heroText { + position: relative; + z-index: 1; + text-align: center; + max-width: 720px; +} + +.heroImageWrap { + position: relative; + z-index: 1; + overflow: hidden; + width: 100%; + mask-image: + linear-gradient(to right, transparent 0%, black 4%, black 96%, transparent 100%), + linear-gradient(to bottom, transparent 0%, black 4%, black 88%, transparent 100%); + mask-composite: intersect; + -webkit-mask-image: + linear-gradient(to right, transparent 0%, black 4%, black 96%, transparent 100%), + linear-gradient(to bottom, transparent 0%, black 4%, black 88%, transparent 100%); + -webkit-mask-composite: source-in; +} + +/* ─── Staggered fade-in animation ────────────────────────────────────────── */ +@keyframes hero-fade-up { + from { + opacity: 0; + transform: translateY(22px); + } + to { + opacity: 1; + transform: none; + } +} + +@keyframes hero-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.heroBadge { + display: inline-flex; + align-items: center; + gap: 7px; + background: var(--landing-teal-light); + color: var(--landing-teal-dark); + font-size: 12px; + font-weight: 500; + padding: 5px 13px; + border-radius: 100px; + margin-bottom: 1.5rem; + border: 0.5px solid var(--landing-teal-mid); + animation: hero-fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.heroBadgeDot { + width: 6px; + height: 6px; + background: var(--landing-teal); + border-radius: 50%; +} + +.hero h1 { + font-family: 'Space Grotesk', sans-serif; + font-size: 62px; + line-height: 1.06; + font-weight: 700; + margin-bottom: 1.25rem; + letter-spacing: -2px; + color: var(--landing-text); + animation: hero-fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.1s both; +} + +.hero h1 em { + color: var(--landing-teal); + font-style: normal; +} + +.heroSub { + font-size: 17px; + color: var(--landing-muted); + line-height: 1.75; + margin-bottom: 2.25rem; + max-width: 540px; + margin-left: auto; + margin-right: auto; + animation: hero-fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.2s both; +} + +.heroActions { + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: center; + animation: hero-fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.3s both; +} + +.heroImageWrap { + animation: hero-fade-up 0.85s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both; +} + +.heroImage { + display: block; + width: 100%; + height: auto; +} + +/* ─── Reduced motion ─────────────────────────────────────────────────────── */ +@media (prefers-reduced-motion: reduce) { + .heroBadge, + .hero h1, + .heroSub, + .heroActions, + .heroImageWrap { + animation: none; + } +} + +/* ─── Responsive ─────────────────────────────────────────────────────────── */ +@media (max-width: 820px) { + .hero { + padding: 3rem 1.5rem 0; + gap: 2rem; + } + + .hero h1 { + font-size: 42px; + letter-spacing: -1.5px; + } + + .heroGlow { + width: 400px; + height: 400px; + top: -40px; + left: -60px; + } +} + +@media (max-width: 560px) { + .heroActions { + flex-direction: column; + } +} diff --git a/src/components/landing/HeroSection.tsx b/src/components/landing/HeroSection.tsx new file mode 100644 index 0000000..d68f75b --- /dev/null +++ b/src/components/landing/HeroSection.tsx @@ -0,0 +1,48 @@ +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Button from './Button'; +import styles from './HeroSection.module.css'; + +export default function HeroSection() { + const heroVideoSrc = useBaseUrl('/videos/artemis-theia-e2e.webm'); + const heroVideoPoster = useBaseUrl('/img/marketing/page.png'); + return ( +
+ {/* Decorative background elements */} +
+ ); +} diff --git a/src/components/landing/InstructorSection.module.css b/src/components/landing/InstructorSection.module.css new file mode 100644 index 0000000..e1001a6 --- /dev/null +++ b/src/components/landing/InstructorSection.module.css @@ -0,0 +1,174 @@ +.section { + padding: 5rem 2rem; + max-width: 1100px; + margin: 0 auto; +} + +.instructorGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: start; +} + +.instructorList { + display: flex; + flex-direction: column; + gap: 1.5rem; + list-style: none; + padding: 0; + margin: 0; +} + +.instItem { + display: flex; + gap: 16px; + align-items: flex-start; +} + +.instNum { + width: 30px; + height: 30px; + border-radius: 50%; + background: var(--landing-orange-light); + color: var(--landing-orange-dark); + font-size: 13px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-top: 1px; +} + +.instItem h4 { + font-size: 1.2rem; + font-weight: 500; + margin-bottom: 5px; + letter-spacing: -0.02em; + color: var(--landing-text); +} + +.instItem p { + font-size: 13px; + color: var(--landing-muted); + line-height: 1.65; +} + +.scorpioImg { + width: 100%; + height: auto; + border-radius: 12px; + border: 0.5px solid var(--landing-border-med); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + align-self: center; +} + +/* Dashboard mockup */ +.dashCard { + background: var(--landing-surface); + border-radius: 16px; + padding: 1.5rem; + border: 0.5px solid var(--landing-border); +} + +.dashHead { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.dashTitle { + font-size: 13px; + font-weight: 600; + color: var(--landing-text); +} + +.dashMeta { + font-size: 12px; + color: var(--landing-hint); +} + +.dashRow { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 11px; + border-radius: 10px; + background: var(--ifm-background-color); + border: 0.5px solid var(--landing-border); + margin-bottom: 7px; +} + +.dashAvatar { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 600; + flex-shrink: 0; +} + +.avT { background: var(--landing-teal-light); color: var(--landing-teal-dark); } +.avO { background: var(--landing-orange-light); color: var(--landing-orange-dark); } +.avB { background: #e8f0ff; color: #3355cc; } +.avG { background: #eaf3de; color: #3a6d11; } + +.dashName { + font-size: 12px; + font-weight: 500; + flex: 1; + color: var(--landing-text); +} + +.dashBadge { + font-size: 10px; + padding: 3px 9px; + border-radius: 100px; + font-weight: 500; +} + +.bDone { background: #eaf3de; color: #3a6d11; } +.bProg { background: #faeeda; color: #854f0b; } +.bErr { background: #fcebeb; color: #a32d2d; } + +.dashBars { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 10px; +} + +.barLabels { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--landing-muted); + margin-bottom: 5px; +} + +.barBg { + height: 6px; + background: var(--landing-border); + border-radius: 3px; +} + +.barFill { + height: 6px; + border-radius: 3px; +} + +.barTeal { background: var(--landing-teal); } +.barOrange { background: var(--landing-orange); } + +@media (max-width: 820px) { + .instructorGrid { + grid-template-columns: 1fr; + gap: 2.5rem; + } +} + diff --git a/src/components/landing/InstructorSection.tsx b/src/components/landing/InstructorSection.tsx new file mode 100644 index 0000000..bcd16f8 --- /dev/null +++ b/src/components/landing/InstructorSection.tsx @@ -0,0 +1,68 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import SectionHeader from "./SectionHeader"; +import { useScrollReveal } from "../../hooks/useScrollReveal"; +import styles from "./InstructorSection.module.css"; + +interface Step { + title: string; + description: string; +} + +const steps: Step[] = [ + { + title: "One-click course setup", + description: + "Define the environment once within Artemis. Every student gets an identical, pre-configured workspace with no need for manual replication.", + }, + { + title: "Automated grading pipeline", + description: + "Integrated with Artemis for test-based grading. Student submissions run against your test suite automatically.", + }, + { + title: 'No "works on my machine" tickets', + description: + "All runtimes, dependencies, and configs live server-side. Support tickets about setup drop to near zero.", + }, +]; + +export default function InstructorSection() { + const sectionRef = useScrollReveal(); + const scorpioImg = useBaseUrl('/img/marketing/scorpio.png'); + + return ( +
+ +
+
    + {steps.map((step, i) => ( +
  1. + +
    +

    {step.title}

    +

    {step.description}

    +
    +
  2. + ))} +
+ Scorpio automated grading pipeline +
+
+ ); +} diff --git a/src/components/landing/LandingNavbar.module.css b/src/components/landing/LandingNavbar.module.css new file mode 100644 index 0000000..dd0e322 --- /dev/null +++ b/src/components/landing/LandingNavbar.module.css @@ -0,0 +1,9 @@ +.nav { + position: sticky; + top: 0; + z-index: var(--ifm-z-index-fixed); +} + +.navLink { + font-weight: 500; +} diff --git a/src/components/landing/LandingNavbar.tsx b/src/components/landing/LandingNavbar.tsx new file mode 100644 index 0000000..3b2e395 --- /dev/null +++ b/src/components/landing/LandingNavbar.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import styles from './LandingNavbar.module.css'; + +export default function LandingNavbar(): React.JSX.Element { + return ( + + ); +} diff --git a/src/components/landing/LanguagesSection.module.css b/src/components/landing/LanguagesSection.module.css new file mode 100644 index 0000000..41c1ad0 --- /dev/null +++ b/src/components/landing/LanguagesSection.module.css @@ -0,0 +1,67 @@ +.section { + padding: 5rem 2rem; + max-width: 1100px; + margin: 0 auto; +} + +.inner { + display: grid; + grid-template-columns: 280px 1fr; + gap: 4rem; + align-items: center; +} + +.textCol h2 { + font-size: 2rem; + font-weight: 600; + line-height: 1.15; + letter-spacing: -0.03em; + color: var(--landing-text); + margin-bottom: 1rem; +} + +.textCol p { + font-size: 16px; + color: var(--landing-muted); + line-height: 1.75; +} + +.langGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + row-gap: 20px; + column-gap: 8px; +} + +.langItem { + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + font-weight: 500; + color: var(--landing-text); +} + +.langLogo { + width: 30px; + height: 30px; + flex-shrink: 0; + object-fit: contain; +} + +@media (max-width: 820px) { + .inner { + grid-template-columns: 1fr; + gap: 2.5rem; + } + + .langGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .langGrid { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/src/components/landing/LanguagesSection.tsx b/src/components/landing/LanguagesSection.tsx new file mode 100644 index 0000000..c95b9d3 --- /dev/null +++ b/src/components/landing/LanguagesSection.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import { useScrollReveal } from '../../hooks/useScrollReveal'; +import styles from './LanguagesSection.module.css'; + +interface Language { + name: string; + logoSrc: string; +} + +const languages: Language[] = [ + { name: 'Java', logoSrc: '/img/marketing/java-svgrepo-com.svg' }, + { name: 'Python', logoSrc: '/img/marketing/python-svgrepo-com.svg' }, + { name: 'C', logoSrc: '/img/marketing/C_Programming_Language.svg' }, + { name: 'JavaScript', logoSrc: '/img/marketing/javascript-svgrepo-com.svg' }, + { name: 'OCaml', logoSrc: '/img/marketing/OCaml_Sticker.svg' }, + { name: 'Rust', logoSrc: '/img/marketing/rust-svgrepo-com.svg' }, +]; + +const LangItem = React.memo<{ language: Language }>(function LangItem({ language }) { + const resolvedSrc = useBaseUrl(language.logoSrc); + return ( +
+ {language.name} + {language.name} +
+ ); +}); + +export default function LanguagesSection() { + const sectionRef = useScrollReveal(); + + return ( +
+
+
+

+ Ready-to-code environments +

+

+ We provide pre-configured Cloud IDEs with full toolchain support and LSP-powered editing right out of the box. +
Enjoy seamless syntax highlighting, autocompletion, and real-time error detection without any setup. +

+
+
+ {languages.map((lang) => ( + + ))} +
+
+
+ ); +} diff --git a/src/components/landing/OpenSourceSection.module.css b/src/components/landing/OpenSourceSection.module.css new file mode 100644 index 0000000..4cd4fb7 --- /dev/null +++ b/src/components/landing/OpenSourceSection.module.css @@ -0,0 +1,216 @@ +.ossSection { + background: var(--landing-surface); + padding: 5rem 2rem; +} + +.ossInner { + max-width: 1100px; + margin: 0 auto; +} + +.ossGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 2rem; + align-items: stretch; +} + +.ossGrid > div { + display: flex; + flex-direction: column; +} + +.ossCard { + flex: 1; + border: 0.5px solid var(--landing-border); + border-radius: 14px; + padding: 1.5rem; + background: var(--ifm-background-color); +} + +.ossCardIcon { + width: 38px; + height: 38px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.85rem; +} + +.iconTeal, +.iconOrange { + color: var(--landing-text); +} + +.ossCardIcon svg { + width: 20px; + height: 20px; +} + +.ossLabel { + font-size: 1.2rem; + font-weight: 500; + margin-bottom: 6px; + letter-spacing: -0.02em; + color: var(--landing-text); +} + +.ossDesc { + font-size: 13px; + color: var(--landing-muted); + line-height: 1.65; +} + +/* Theia prominent card */ +.ossTheiaProminent { + border: 0.5px solid var(--landing-border-med); + border-radius: 16px; + padding: 2rem 2.25rem; + background: var(--ifm-background-color); + display: grid; + grid-template-columns: 1fr auto; + gap: 2.5rem; + align-items: start; + margin-bottom: 1.5rem; +} + +.ossTheiaLeft { + display: flex; + gap: 1.25rem; + align-items: flex-start; +} + +.ossTheiaBadge { + flex-shrink: 0; +} + +.ossTheiaBadge img { + width: 52px; + height: 52px; + object-fit: contain; +} + +.ossTheiaTag { + font-size: 11px; + font-weight: 500; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--landing-teal); + margin-bottom: 6px; + display: inline-block; +} + +.ossTheiaLink { + display: inline-block; + margin-top: 1rem; + padding: 0.4rem 0.9rem; + border: 0.5px solid var(--landing-border-med); + border-radius: 8px; + font-size: 13px; + color: var(--landing-muted); + text-decoration: none; +} + +.ossTheiaLink:hover { + color: var(--landing-text); + border-color: var(--landing-teal); +} + +.ossTheiaTitle { + font-size: 24px; + font-weight: 400; + line-height: 1.2; + margin-bottom: 10px; + color: var(--landing-text); +} + +.ossTheiaDesc { + font-size: 14px; + color: var(--landing-muted); + line-height: 1.75; + max-width: 520px; +} + +.ossContribCard { + background: var(--landing-surface); + border-radius: 12px; + padding: 1.1rem 1.25rem; + border: 0.5px solid var(--landing-border); + min-width: 220px; +} + +.ossContribTitle { + font-size: 12px; + font-weight: 600; + color: var(--landing-muted); + margin-bottom: 12px; + letter-spacing: 0.03em; +} + +.contribRow { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 0; + font-size: 12px; + color: var(--landing-muted); +} + +.contribDot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + +.contribText { + line-height: 1.4; +} + +.contribFooter { + margin-top: 14px; + padding-top: 12px; + border-top: 0.5px solid var(--landing-border); + font-size: 11px; + color: var(--landing-hint); +} + +.ossTags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 0.5rem; +} + +.ossTag { + font-size: 12px; + padding: 4px 11px; + border-radius: 6px; + background: var(--ifm-background-color); + color: var(--landing-muted); + border: 0.5px solid var(--landing-border-med); + font-family: 'Courier New', monospace; +} + +@media (max-width: 820px) { + .ossGrid { + grid-template-columns: 1fr 1fr; + } + + .ossTheiaProminent { + grid-template-columns: 1fr; + gap: 1.5rem; + } +} + +@media (max-width: 560px) { + .ossGrid { + grid-template-columns: 1fr; + } + + .ossTheiaLeft { + flex-direction: column; + } +} diff --git a/src/components/landing/OpenSourceSection.tsx b/src/components/landing/OpenSourceSection.tsx new file mode 100644 index 0000000..c179f3b --- /dev/null +++ b/src/components/landing/OpenSourceSection.tsx @@ -0,0 +1,156 @@ +import React from "react"; +import type { ReactNode } from "react"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import SectionHeader from "./SectionHeader"; +import { useScrollReveal } from "../../hooks/useScrollReveal"; +import styles from "./OpenSourceSection.module.css"; + +interface OssCard { + iconColor: "teal" | "orange"; + icon: ReactNode; + title: string; + description: string; +} + +interface Contribution { + color: string; + text: string; +} + +/* Codicon: source-control (24x24) */ +const IconSourceControl = () => ( + + + +); + +/* Codicon: heart (16x16) */ +const IconHeart = () => ( + + + +); + +/* Codicon: book (16x16) */ +const IconBook = () => ( + + + +); + +const cards: OssCard[] = [ + { + iconColor: "teal", + icon: , + title: "Open source", + description: + "The platform is open source and freely available. Institutions can fork the repository to customize the environment or audit the security and data practices directly.", + }, + { + iconColor: "orange", + icon: , + title: "Built by students, for students", + description: + "EduIDE is developed as part of thesis projects at TUM's AET Chair — by students who understand the learning experience firsthand.", + }, + { + iconColor: "teal", + icon: , + title: "Education research-backed", + description: + "Features are grounded in real student studies and pedagogical research at TUM's Applied Education Technologies chair.", + }, +]; + +const contributions: Contribution[] = [ + { color: "#4E9F9C", text: "AI terminal assistant extension" }, + { color: "#4E9F9C", text: "OSC Escape Sequence Support in the Terminal" }, + { color: "#4E9F9C", text: "Git Action button support" }, + { color: "#4E9F9C", text: "And more" }, +]; + +const OssCardItem = React.memo<{ card: OssCard }>(function OssCardItem({ + card, +}) { + return ( +
+ +
{card.title}
+
{card.description}
+
+ ); +}); + +export default function OpenSourceSection() { + const sectionRef = useScrollReveal(); + const theiaLogo = useBaseUrl("/img/theia.svg"); + + return ( +
+
+ + +
+ {cards.map((card) => ( + + ))} +
+ +
+
+ +
+
Powered by Eclipse Theia
+

+ Built on a Professional Foundation +

+

+ EduIDE is built on Eclipse Theia, the same robust, open-source framework behind many professional cloud IDEs. + Through our close collaboration with EclipseSource, we leverage a highly scalable foundation and actively contribute our educational extensions back to the upstream community. +

+ Visit theia-ide.org +
+
+
+
Contributions upstream
+ {contributions.map((c) => ( +
+ + ))} +
+ Part of the Eclipse Foundation ecosystem +
+
+
+
+
+ ); +} diff --git a/src/components/landing/SectionHeader.module.css b/src/components/landing/SectionHeader.module.css new file mode 100644 index 0000000..2b4e1c5 --- /dev/null +++ b/src/components/landing/SectionHeader.module.css @@ -0,0 +1,32 @@ +.sectionLabel { + font-size: 12px; + font-weight: 500; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--landing-teal); + margin-bottom: 0.75rem; +} + +.sectionTitle { + font-size: 2.5rem; + font-weight: 500; + line-height: 1.1; + letter-spacing: -0.04em; + text-wrap: balance; + margin-bottom: 0.85rem; + color: var(--landing-text); +} + +.sectionSub { + font-size: 16px; + color: var(--landing-muted); + line-height: 1.75; + max-width: 540px; + margin-bottom: 3rem; +} + +@media (max-width: 560px) { + .sectionTitle { + font-size: 1.85rem; + } +} diff --git a/src/components/landing/SectionHeader.tsx b/src/components/landing/SectionHeader.tsx new file mode 100644 index 0000000..bafc3eb --- /dev/null +++ b/src/components/landing/SectionHeader.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import styles from './SectionHeader.module.css'; + +interface SectionHeaderProps { + label: string; + title: string; + subtitle: string; + id?: string; +} + +const SectionHeader = React.memo(function SectionHeader({ + label, + title, + subtitle, + id, +}) { + return ( + <> +
{label}
+

{title}

+

{subtitle}

+ + ); +}); + +export default SectionHeader; diff --git a/src/components/landing/StudentFeatures.module.css b/src/components/landing/StudentFeatures.module.css new file mode 100644 index 0000000..8b4022f --- /dev/null +++ b/src/components/landing/StudentFeatures.module.css @@ -0,0 +1,96 @@ +.section { + padding: 5rem 2rem; + max-width: 1100px; + margin: 0 auto; +} + +.featuresGrid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + align-items: stretch; +} + +.featuresGrid > div { + display: flex; + flex-direction: column; +} + +.featureCard { + flex: 1; + border: 0.5px solid var(--landing-border); + border-radius: 14px; + padding: 1.4rem; + background: var(--ifm-background-color); + transition: border-color 0.15s; +} + +.featureCard:hover { + border-color: var(--landing-border-med); +} + +.featureCard.highlight { + border-color: var(--landing-teal); + border-width: 1.5px; +} + +.featurePill { + font-size: 10px; + font-weight: 600; + color: var(--landing-teal-dark); + background: var(--landing-teal-light); + padding: 3px 9px; + border-radius: 100px; + letter-spacing: 0.05em; + text-transform: uppercase; + margin-bottom: 10px; + display: inline-block; +} + +.featureIcon { + width: 38px; + height: 38px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.85rem; + flex-shrink: 0; +} + +.featureIcon svg { + width: 20px; + height: 20px; +} + +.iconTeal, +.iconOrange { + color: var(--landing-text); +} + +.featureCard h3 { + font-size: 1.2rem; + font-weight: 500; + margin-bottom: 6px; + line-height: 1.3; + letter-spacing: -0.02em; + color: var(--landing-text); +} + +.featureCard p { + font-size: 16px; + color: var(--landing-muted); + line-height: 1.65; +} + +@media (max-width: 820px) { + .featuresGrid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 560px) { + .featuresGrid { + grid-template-columns: 1fr; + } +} diff --git a/src/components/landing/StudentFeatures.tsx b/src/components/landing/StudentFeatures.tsx new file mode 100644 index 0000000..4f966e0 --- /dev/null +++ b/src/components/landing/StudentFeatures.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import type { ReactNode } from "react"; +import SectionHeader from "./SectionHeader"; +import { useScrollReveal } from "../../hooks/useScrollReveal"; +import styles from "./StudentFeatures.module.css"; + +interface Feature { + icon: ReactNode; + title: string; + description: string; + iconColor: "teal" | "orange"; + highlight?: boolean; + pill?: string; +} + +/* Codicon: clockface (16x16) */ +const IconClock = () => ( + + + +); + +/* Codicon: list-ordered (16x16) — horizontal bars on the right side */ +const IconListOrdered = () => ( + + + +); + +/* Codicon: server (16x16) */ +const IconServer = () => ( + + + +); + +const features: Feature[] = [ + { + iconColor: "teal", + icon: , + title: "Zero setup", + description: + "Open your browser. Your full dev environment is ready — no installs, no PATH issues, no Docker to configure.", + }, + { + iconColor: "orange", + icon: , + title: "Assignment integration", + description: + "Assignments appear directly in the IDE. Submit without switching tabs or manually uploading files.", + }, + { + iconColor: "teal", + icon: , + title: "Consistent environments", + description: + "Every student runs identical toolchains. What works for you works for the grader — every single time.", + }, +]; + +interface FeatureCardProps { + feature: Feature; +} + +const FeatureCard = React.memo(function FeatureCard({ + feature, +}) { + return ( +
+ {feature.pill &&
{feature.pill}
} + +

{feature.title}

+

{feature.description}

+
+ ); +}); + +function RevealCard({ feature, delay }: { feature: Feature; delay: number }) { + const ref = useScrollReveal(delay); + return ( +
+ +
+ ); +} + +export default function StudentFeatures() { + return ( +
+ +
+ {features.map((f, i) => ( + + ))} +
+
+ ); +} diff --git a/src/css/custom.css b/src/css/custom.css index d298e4f..e6f6e71 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -4,6 +4,64 @@ * work well for content-centric websites. */ +/* ─── Local fonts ────────────────────────────────────────────────────────── */ +@font-face { + font-family: 'DM Sans'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(../fonts/rP2rp2ywxg089UriCZaSExd86J3t9jz86Mvy4qCRAL19DksVat-JDV36TGc5pg.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'DM Sans'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(../fonts/rP2rp2ywxg089UriCZaSExd86J3t9jz86Mvy4qCRAL19DksVat-JDV30TGc.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'DM Sans'; + font-style: normal; + font-weight: 400 600; + font-display: swap; + src: url(../fonts/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu6-K6h9Q.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'DM Sans'; + font-style: normal; + font-weight: 400 600; + font-display: swap; + src: url(../fonts/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 600 700; + font-display: swap; + src: url(../fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 600 700; + font-display: swap; + src: url(../fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 600 700; + font-display: swap; + src: url(../fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + /* You can override the default Infima variables here. */ :root { --ifm-color-primary: #8bb4ff; @@ -15,6 +73,44 @@ --ifm-color-primary-lightest: #ffffff; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + + /* Landing page design tokens */ + --landing-teal: #4E9F9C; + --landing-teal-light: #e8f5f5; + --landing-teal-mid: #b2d9d8; + --landing-teal-dark: #3a7a78; + --landing-orange: #E88733; + --landing-orange-light: #fdf3e7; + --landing-orange-dark: #c46d1a; + --landing-surface: #f7f7f5; + --landing-border: rgba(0, 0, 0, 0.09); + --landing-border-med: rgba(0, 0, 0, 0.16); + --landing-text: #1a1a1a; + --landing-muted: #6b6b6b; + --landing-hint: #9a9a9a; + --landing-grid-line: rgba(78, 159, 156, 0.07); +} + +/* ─── Scroll reveal ──────────────────────────────────────────────────────── */ +.reveal { + opacity: 0; + transform: translateY(28px); + transition: + opacity 0.65s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.65s cubic-bezier(0.16, 1, 0.3, 1); +} + +.reveal.revealed { + opacity: 1; + transform: none; +} + +@media (prefers-reduced-motion: reduce) { + .reveal { + opacity: 1 !important; + transform: none !important; + transition: none !important; + } } /* For readability concerns, you should choose a lighter palette in dark mode. */ @@ -28,6 +124,21 @@ --ifm-color-primary-lightest: #ffffff; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); --ifm-background-color: #2a2a40; + + --landing-teal: #6db5b2; + --landing-teal-light: rgba(78, 159, 156, 0.15); + --landing-teal-mid: rgba(78, 159, 156, 0.3); + --landing-teal-dark: #8bc5c3; + --landing-orange: #f0a060; + --landing-orange-light: rgba(232, 135, 51, 0.15); + --landing-orange-dark: #f0a060; + --landing-surface: rgba(255, 255, 255, 0.04); + --landing-border: rgba(255, 255, 255, 0.08); + --landing-border-med: rgba(255, 255, 255, 0.14); + --landing-text: #e2e8f0; + --landing-muted: #94a3b8; + --landing-hint: #64748b; + --landing-grid-line: rgba(78, 159, 156, 0.1); } [data-theme='dark'] .button.button--secondary:not(#_):not(#_) { diff --git a/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2 b/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2 new file mode 100644 index 0000000..ce97d0f Binary files /dev/null and b/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb54C-s0.woff2 differ diff --git a/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2 b/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2 new file mode 100644 index 0000000..db732c2 Binary files /dev/null and b/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPb94C-s0.woff2 differ diff --git a/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2 b/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2 new file mode 100644 index 0000000..0f3474e Binary files /dev/null and b/src/fonts/V8mDoQDjQSkFtoMM3T6r8E7mPbF4Cw.woff2 differ diff --git a/src/fonts/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2 b/src/fonts/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2 new file mode 100644 index 0000000..01383d7 Binary files /dev/null and b/src/fonts/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2 differ diff --git a/src/fonts/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu6-K6h9Q.woff2 b/src/fonts/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu6-K6h9Q.woff2 new file mode 100644 index 0000000..db39c62 Binary files /dev/null and b/src/fonts/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu6-K6h9Q.woff2 differ diff --git a/src/fonts/rP2rp2ywxg089UriCZaSExd86J3t9jz86Mvy4qCRAL19DksVat-JDV30TGc.woff2 b/src/fonts/rP2rp2ywxg089UriCZaSExd86J3t9jz86Mvy4qCRAL19DksVat-JDV30TGc.woff2 new file mode 100644 index 0000000..d7898c9 Binary files /dev/null and b/src/fonts/rP2rp2ywxg089UriCZaSExd86J3t9jz86Mvy4qCRAL19DksVat-JDV30TGc.woff2 differ diff --git a/src/fonts/rP2rp2ywxg089UriCZaSExd86J3t9jz86Mvy4qCRAL19DksVat-JDV36TGc5pg.woff2 b/src/fonts/rP2rp2ywxg089UriCZaSExd86J3t9jz86Mvy4qCRAL19DksVat-JDV36TGc5pg.woff2 new file mode 100644 index 0000000..7f00609 Binary files /dev/null and b/src/fonts/rP2rp2ywxg089UriCZaSExd86J3t9jz86Mvy4qCRAL19DksVat-JDV36TGc5pg.woff2 differ diff --git a/src/hooks/useScrollReveal.ts b/src/hooks/useScrollReveal.ts new file mode 100644 index 0000000..fff11d7 --- /dev/null +++ b/src/hooks/useScrollReveal.ts @@ -0,0 +1,39 @@ +import { useEffect, useRef, type RefObject } from 'react'; + +/** + * Adds `.reveal` on mount and `.revealed` once the element enters the viewport. + * Disconnects the observer after triggering so it only fires once. + */ +export function useScrollReveal( + delay: number = 0, +): RefObject { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + el.classList.add('reveal'); + + let timerId: ReturnType | undefined; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + if (delay > 0) { + timerId = setTimeout(() => el.classList.add('revealed'), delay); + } else { + el.classList.add('revealed'); + } + observer.disconnect(); + } + }, + { threshold: 0.1, rootMargin: '0px 0px -48px 0px' }, + ); + + observer.observe(el); + return () => { observer.disconnect(); clearTimeout(timerId); }; + }, [delay]); + + return ref; +} diff --git a/src/pages/index.module.css b/src/pages/index.module.css index 8bc0fdf..7974fd2 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -1,252 +1,13 @@ .page { - background: - radial-gradient(circle at top left, rgba(15, 99, 255, 0.24), transparent 28%), - radial-gradient(circle at top right, rgba(10, 184, 126, 0.18), transparent 24%), - linear-gradient(180deg, #f7f8fc 0%, #eef3ff 54%, #ffffff 100%); + font-family: 'DM Sans', sans-serif; + color: var(--landing-text); + background: var(--ifm-background-color); + overflow-x: hidden; } -.hero { - position: relative; - display: grid; - grid-template-columns: minmax(0, 1.4fr) minmax(280px, 0.9fr); - gap: 2rem; - max-width: 1200px; +.sectionDivider { + border: none; + border-top: 0.5px solid var(--landing-border); + max-width: 1100px; margin: 0 auto; - padding: 5rem 1.5rem 4rem; - overflow: hidden; -} - -.heroGlow { - position: absolute; - inset: 0; - background: - radial-gradient(circle at 20% 15%, rgba(255, 255, 255, 0.95), transparent 26%), - radial-gradient(circle at 75% 30%, rgba(73, 123, 255, 0.18), transparent 22%); - pointer-events: none; -} - -.heroContent, -.heroPanel { - position: relative; - z-index: 1; -} - -.kicker { - margin: 0 0 1rem; - color: #0f63ff; - font-size: 0.85rem; - font-weight: 700; - letter-spacing: 0.16em; - text-transform: uppercase; -} - -.heroContent h1 { - max-width: 12ch; - margin: 0; - color: #0f172a; - font-size: clamp(3rem, 8vw, 5.75rem); - line-height: 0.94; -} - -.lead { - max-width: 44rem; - margin: 1.5rem 0 0; - color: #334155; - font-size: 1.15rem; -} - -.actions { - display: flex; - flex-wrap: wrap; - gap: 1rem; - margin-top: 2rem; -} - -.secondaryAction { - border-color: rgba(15, 23, 42, 0.08); - background: rgba(255, 255, 255, 0.72); - color: #0f172a; -} - -.heroPanel { - display: grid; - align-content: center; - gap: 1rem; -} - -.metricCard { - padding: 1.4rem; - border: 1px solid rgba(15, 23, 42, 0.08); - border-radius: 1.25rem; - background: rgba(255, 255, 255, 0.8); - box-shadow: 0 20px 45px rgba(15, 23, 42, 0.08); - backdrop-filter: blur(14px); -} - -.metricCard span { - display: block; - margin-bottom: 0.45rem; - color: #64748b; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.metricCard strong { - color: #0f172a; - font-size: 1.4rem; -} - -.sectionGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 1rem; - max-width: 1200px; - margin: 0 auto; - padding: 0 1.5rem 5rem; -} - -.sectionCard { - display: block; - min-height: 250px; - padding: 1.5rem; - border: 1px solid rgba(15, 23, 42, 0.08); - border-radius: 1.5rem; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(242, 246, 255, 0.84)), - #ffffff; - color: inherit; - box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08); - text-decoration: none; - transition: - transform 180ms ease, - box-shadow 180ms ease, - border-color 180ms ease; -} - -.sectionCard:hover { - border-color: rgba(15, 99, 255, 0.3); - box-shadow: 0 24px 55px rgba(15, 99, 255, 0.14); - transform: translateY(-4px); -} - -.sectionLabel { - color: #0f63ff; - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -.sectionCard h2 { - margin: 0.9rem 0 0.75rem; - color: #0f172a; -} - -.sectionCard p { - color: #475569; -} - -.sectionCta { - display: inline-block; - margin-top: 1.25rem; - color: #0f172a; - font-weight: 700; -} - -@media (max-width: 996px) { - .hero { - grid-template-columns: 1fr; - padding-top: 3.5rem; - } - - .heroContent h1 { - max-width: none; - } - - .sectionGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -[data-theme='dark'] .page { - background: - radial-gradient(circle at top left, rgba(15, 99, 255, 0.18), transparent 28%), - radial-gradient(circle at top right, rgba(10, 184, 126, 0.12), transparent 24%), - linear-gradient(180deg, #1e1e2e 0%, #1a1a2e 54%, #2a2a40 100%); -} - -[data-theme='dark'] .heroGlow { - background: - radial-gradient(circle at 20% 15%, rgba(30, 30, 46, 0.8), transparent 26%), - radial-gradient(circle at 75% 30%, rgba(73, 123, 255, 0.12), transparent 22%); -} - -[data-theme='dark'] .kicker { - color: #8bb4ff; -} - -[data-theme='dark'] .heroContent h1 { - color: #e2e8f0; -} - -[data-theme='dark'] .lead { - color: #94a3b8; -} - -[data-theme='dark'] .secondaryAction { - border-color: rgba(255, 255, 255, 0.12); - color: #e2e8f0; -} - -[data-theme='dark'] .metricCard { - border-color: rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.06); - box-shadow: 0 20px 45px rgba(0, 0, 0, 0.3); -} - -[data-theme='dark'] .metricCard span { - color: #94a3b8; -} - -[data-theme='dark'] .metricCard strong { - color: #e2e8f0; -} - -[data-theme='dark'] .sectionCard { - border-color: rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.06); - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.3); -} - -[data-theme='dark'] .sectionCard:hover { - border-color: rgba(139, 180, 255, 0.4); - box-shadow: 0 24px 55px rgba(139, 180, 255, 0.1); -} - -[data-theme='dark'] .sectionLabel { - color: #8bb4ff; -} - -[data-theme='dark'] .sectionCard h2 { - color: #e2e8f0; -} - -[data-theme='dark'] .sectionCard p { - color: #94a3b8; -} - -[data-theme='dark'] .sectionCta { - color: #e2e8f0; -} - -@media (max-width: 640px) { - .actions { - flex-direction: column; - } - - .sectionGrid { - grid-template-columns: 1fr; - padding-bottom: 4rem; - } } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 3a6dacc..7dc22af 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,83 +1,63 @@ -import clsx from 'clsx'; import Layout from '@theme/Layout'; -import Link from '@docusaurus/Link'; +import HeroSection from '../components/landing/HeroSection'; +import StudentFeatures from '../components/landing/StudentFeatures'; +import InstructorSection from '../components/landing/InstructorSection'; +import FeatureShowcase from '../components/landing/FeatureShowcase'; +import LanguagesSection from '../components/landing/LanguagesSection'; +import OpenSourceSection from '../components/landing/OpenSourceSection'; +import CtaSection from '../components/landing/CtaSection'; import styles from './index.module.css'; -const sections = [ - { - title: 'Instructor', - description: 'Course delivery workflows, classroom playbooks, and content operations guidance.', - href: '/instructor/intro', - }, - { - title: 'Student', - description: 'Onboarding, workspace basics, assignment flow, and submission support.', - href: '/student/intro', - }, - { - title: 'Admins', - description: 'Provisioning, permissions, platform operations, and incident handling documentation.', - href: '/admins/intro', - }, - { - title: 'Developer', - description: 'API references, architecture decisions, integration guides, and implementation patterns.', - href: '/developer/intro', - }, -]; - export default function Home() { return (
-
-
-
-

Learning infrastructure for modern engineering education

-

EduIDE brings product storytelling and practical docs into one place.

-

- This landing page can sell the platform, while every audience gets a dedicated doc space with - its own sidebar, route, and navigation path. -

-
- - Open docs - - - Explore student flow - -
-
-
-
- Audience-based docs - 4 sections -
-
- Navigation model - Independent sidebars -
-
- Current state - Mock content ready -
-
-
- -
- {sections.map((section) => ( - - Section -

{section.title}

-

{section.description}

- Enter section - - ))} -
+ + +
+ +
+ +
+ +
+ +
+ + +
); diff --git a/src/theme/Navbar/index.tsx b/src/theme/Navbar/index.tsx new file mode 100644 index 0000000..cc86d1a --- /dev/null +++ b/src/theme/Navbar/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useLocation } from '@docusaurus/router'; +import NavbarOriginal from '@theme-original/Navbar'; +import type NavbarType from '@theme/Navbar'; +import type { WrapperProps } from '@docusaurus/types'; +import LandingNavbar from '../../components/landing/LandingNavbar'; + +type Props = WrapperProps; + +export default function NavbarWrapper(props: Props): React.JSX.Element { + const { pathname } = useLocation(); + const isHome = pathname === '/' || pathname === '/Docs/' || pathname === '/Docs'; + + if (isHome) { + return ; + } + return ; +} diff --git a/static/img/marketing/C_Programming_Language.svg b/static/img/marketing/C_Programming_Language.svg new file mode 100644 index 0000000..28c30fc --- /dev/null +++ b/static/img/marketing/C_Programming_Language.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/static/img/marketing/OCaml_Sticker.svg b/static/img/marketing/OCaml_Sticker.svg new file mode 100644 index 0000000..beab0ed --- /dev/null +++ b/static/img/marketing/OCaml_Sticker.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/marketing/java-svgrepo-com.svg b/static/img/marketing/java-svgrepo-com.svg new file mode 100644 index 0000000..ed92b9a --- /dev/null +++ b/static/img/marketing/java-svgrepo-com.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/marketing/javascript-svgrepo-com.svg b/static/img/marketing/javascript-svgrepo-com.svg new file mode 100644 index 0000000..b2d9844 --- /dev/null +++ b/static/img/marketing/javascript-svgrepo-com.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/static/img/marketing/page.png b/static/img/marketing/page.png new file mode 100644 index 0000000..80a390e Binary files /dev/null and b/static/img/marketing/page.png differ diff --git a/static/img/marketing/python-svgrepo-com.svg b/static/img/marketing/python-svgrepo-com.svg new file mode 100644 index 0000000..3a4e654 --- /dev/null +++ b/static/img/marketing/python-svgrepo-com.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/marketing/rust-svgrepo-com.svg b/static/img/marketing/rust-svgrepo-com.svg new file mode 100644 index 0000000..d411cb1 --- /dev/null +++ b/static/img/marketing/rust-svgrepo-com.svg @@ -0,0 +1,6 @@ + + + +rust + + \ No newline at end of file diff --git a/static/img/marketing/scorpio.png b/static/img/marketing/scorpio.png new file mode 100644 index 0000000..a95617f Binary files /dev/null and b/static/img/marketing/scorpio.png differ diff --git a/static/img/theia.svg b/static/img/theia.svg new file mode 100644 index 0000000..f02ea66 --- /dev/null +++ b/static/img/theia.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/videos/artemis-theia-e2e.webm b/static/videos/artemis-theia-e2e.webm new file mode 100644 index 0000000..3d5bb42 Binary files /dev/null and b/static/videos/artemis-theia-e2e.webm differ diff --git a/static/videos/run-button.webm b/static/videos/run-button.webm new file mode 100644 index 0000000..acb9b6e Binary files /dev/null and b/static/videos/run-button.webm differ diff --git a/static/videos/scorpio.webm b/static/videos/scorpio.webm new file mode 100644 index 0000000..eb57d0b Binary files /dev/null and b/static/videos/scorpio.webm differ diff --git a/static/videos/terminal-assistant.webm b/static/videos/terminal-assistant.webm new file mode 100644 index 0000000..246dfbb Binary files /dev/null and b/static/videos/terminal-assistant.webm differ