diff --git a/docs/todo.md b/docs/todo.md index 5b792bb..4881869 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -63,11 +63,11 @@ **Marketing/Static Site:** -- [ ] Landing page with hero section, feature highlights, social proof - - Hero should have a graph paper/grid background - - Floating flash cards, notes -- [ ] "How it works" section with study flow visualization -- [ ] About page with team/mission +- [x] Landing page with hero section, feature highlights + - Hero has graph paper/grid background + - Floating flash cards, notes animations +- [x] "How it works" section with study flow visualization +- [x] About page with team/mission **App Vision Content:** diff --git a/web/package.json b/web/package.json index 0e9c2c4..8852d83 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,8 @@ "lint": "eslint ." }, "dependencies": { + "@fontsource-variable/alegreya": "^5.2.8", + "@fontsource-variable/lora": "^5.2.8", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.4", "@tailwindcss/vite": "^4.1.18", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 4450a7d..047965c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: .: dependencies: + '@fontsource-variable/alegreya': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource-variable/lora': + specifier: ^5.2.8 + version: 5.2.8 '@solidjs/meta': specifier: ^0.29.4 version: 0.29.4(solid-js@1.9.10) @@ -310,6 +316,12 @@ packages: '@exodus/crypto': optional: true + '@fontsource-variable/alegreya@5.2.8': + resolution: {integrity: sha512-gQcIA7j76KYTOcdkfo1Xee9xLBi5mya4qTkzlgeoHf9SjOL/gJj5GSSOg/7ba/ciUU18K92i7VGxXzFhDsowGg==} + + '@fontsource-variable/lora@5.2.8': + resolution: {integrity: sha512-cxjTJ9BbOWIzusewR4UMBLVePvTSWV6dtNaNsCkF/oKoyA68fJGWfaYCILOOP1BObE4dmjfZ3xo6m9hdHhtYhg==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2160,6 +2172,10 @@ snapshots: '@exodus/bytes@1.6.0': {} + '@fontsource-variable/alegreya@5.2.8': {} + + '@fontsource-variable/lora@5.2.8': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': diff --git a/web/public/tangled.svg b/web/public/tangled.svg new file mode 100644 index 0000000..ea3c34b --- /dev/null +++ b/web/public/tangled.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/App.tsx b/web/src/App.tsx index 0b63e6e..3bc59fd 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,6 @@ import { AppLayout } from "$components/layout/AppLayout"; import { authStore } from "$lib/store"; +import About from "$pages/About"; import DeckNew from "$pages/DeckNew"; import DeckView from "$pages/DeckView"; import Discovery from "$pages/Discovery"; @@ -31,6 +32,7 @@ const App: Component = () => { return ( + } /> } /> } /> diff --git a/web/src/components/layout/AppLayout.tsx b/web/src/components/layout/AppLayout.tsx index cb35a66..7abe8c1 100644 --- a/web/src/components/layout/AppLayout.tsx +++ b/web/src/components/layout/AppLayout.tsx @@ -1,15 +1,13 @@ import { Toaster } from "$ui/Toast"; import type { Component, JSX } from "solid-js"; +import { Footer } from "./Footer"; import { Header } from "./Header"; -type AppLayoutProps = { children?: JSX.Element }; - -export const AppLayout: Component = (props) => { - return ( -
-
-
{props.children}
- -
- ); -}; +export const AppLayout: Component<{ children?: JSX.Element }> = (props) => ( +
+
+
{props.children}
+
+ +
+); diff --git a/web/src/components/layout/Footer.test.tsx b/web/src/components/layout/Footer.test.tsx new file mode 100644 index 0000000..2a9cd5e --- /dev/null +++ b/web/src/components/layout/Footer.test.tsx @@ -0,0 +1,39 @@ +import { MemoryRouter, Route } from "@solidjs/router"; +import { cleanup, render, screen } from "@solidjs/testing-library"; +import { afterEach, describe, expect, it } from "vitest"; +import { Footer } from "./Footer"; + +describe("Footer", () => { + afterEach(cleanup); + + function renderFooter() { + render(() => ( + + + + )); + } + + it("renders copyright text", () => { + renderFooter(); + expect(screen.getByText(/© 2025 Stormlight Labs/)).toBeInTheDocument(); + }); + + it("renders About link", () => { + renderFooter(); + const aboutLink = screen.getByRole("link", { name: "About" }); + expect(aboutLink).toHaveAttribute("href", "/about"); + }); + + it("renders Tangled link", () => { + renderFooter(); + const tangledLink = screen.getByRole("link", { name: "Tangled" }); + expect(tangledLink).toHaveAttribute("href", "https://tangled.org/desertthunder.dev/malfestio"); + }); + + it("renders GitHub link", () => { + renderFooter(); + const githubLink = screen.getByRole("link", { name: "GitHub" }); + expect(githubLink).toHaveAttribute("href", "https://github.com/stormlightlabs"); + }); +}); diff --git a/web/src/components/layout/Footer.tsx b/web/src/components/layout/Footer.tsx new file mode 100644 index 0000000..6d8598f --- /dev/null +++ b/web/src/components/layout/Footer.tsx @@ -0,0 +1,46 @@ +import { A } from "@solidjs/router"; +import type { Component } from "solid-js"; + +const TangledIcon: Component<{ class?: string }> = (props) => ( + + + +); + +export const Footer: Component = () => ( + +); diff --git a/web/src/fonts.d.ts b/web/src/fonts.d.ts new file mode 100644 index 0000000..b9a63f0 --- /dev/null +++ b/web/src/fonts.d.ts @@ -0,0 +1,3 @@ +// CSS-only packages without TypeScript declarations +declare module "@fontsource-variable/alegreya"; +declare module "@fontsource-variable/lora"; diff --git a/web/src/index.css b/web/src/index.css index 0c8e0d4..2378ab5 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,2 +1,30 @@ @import "tailwindcss"; @plugin "@egoist/tailwindcss-icons"; + +@theme { + --font-display: "Lora Variable", serif; + --font-body: "Alegreya Variable", serif; +} + +* { + @apply font-body; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + @apply font-display; +} + +.grid-pattern { + background-size: 32px 32px; + background-image: linear-gradient( + to right, + rgba(85, 85, 85, 0.3) 1px, + transparent 1px + ), + linear-gradient(to bottom, rgba(85, 85, 85, 0.3) 1px, transparent 1px); +} diff --git a/web/src/index.tsx b/web/src/index.tsx index e21c637..ba09891 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,4 +1,6 @@ /* @refresh reload */ +import "@fontsource-variable/alegreya"; +import "@fontsource-variable/lora"; import { render } from "solid-js/web"; import "./index.css"; import App from "./App.tsx"; diff --git a/web/src/pages/About.test.tsx b/web/src/pages/About.test.tsx new file mode 100644 index 0000000..6621ea3 --- /dev/null +++ b/web/src/pages/About.test.tsx @@ -0,0 +1,45 @@ +import { MemoryRouter, Route } from "@solidjs/router"; +import { cleanup, render, screen } from "@solidjs/testing-library"; +import { afterEach, describe, expect, it } from "vitest"; +import About from "./About"; + +describe("About Page", () => { + afterEach(cleanup); + + function renderAbout() { + render(() => ( + + + + )); + } + + it("renders page title", () => { + renderAbout(); + expect(screen.getByText("About Malfestio")).toBeInTheDocument(); + }); + + it("renders mission statement", () => { + renderAbout(); + expect(screen.getByText(/decentralized learning platform/i)).toBeInTheDocument(); + }); + + it("renders team section with Owais", () => { + renderAbout(); + expect(screen.getByText("Team")).toBeInTheDocument(); + expect(screen.getByText("Owais")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "desertthunder.dev" })).toHaveAttribute( + "href", + "https://desertthunder.dev", + ); + }); + + it("renders links section", () => { + renderAbout(); + expect(screen.getByText("Links")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: /Tangled Repository/i })).toHaveAttribute( + "href", + "https://tangled.org/desertthunder.dev/malfestio", + ); + }); +}); diff --git a/web/src/pages/About.tsx b/web/src/pages/About.tsx new file mode 100644 index 0000000..73b8fec --- /dev/null +++ b/web/src/pages/About.tsx @@ -0,0 +1,92 @@ +import { Footer } from "$components/layout/Footer"; +import { A } from "@solidjs/router"; +import type { Component } from "solid-js"; + +const About: Component = () => ( +
+
+
+ Malfestio +
+ About + Log in +
+
+
+ +
+
+

About Malfestio

+

+ Malfestio is a decentralized learning platform built on the AT Protocol. We believe knowledge should be + portable, shareable, and owned by learners — not locked into proprietary platforms. +

+

+ By combining spaced repetition, linked notes, and social features with decentralized identity and data + ownership, we're creating a new way to learn and share knowledge across the open web. +

+
+ +
+

Team

+
+
+
+ O +
+
+

Owais

+

+ + desertthunder.dev + +

+
+
+

+ Building at Stormlight Labs. Passionate about decentralized systems, learning tools, and the AT Protocol. +

+
+
+ +
+

Links

+ +
+
+ +
+
+); + +export default About; diff --git a/web/src/pages/Landing.test.tsx b/web/src/pages/Landing.test.tsx index 26d5513..54ab70f 100644 --- a/web/src/pages/Landing.test.tsx +++ b/web/src/pages/Landing.test.tsx @@ -17,7 +17,7 @@ describe("Landing Page", () => { renderLanding(); expect(screen.getByText(/Learning on/i)).toBeInTheDocument(); - expect(screen.getByText(/the AT Protocol/i)).toBeInTheDocument(); + expect(screen.getAllByText(/the AT Protocol/i).length).toBeGreaterThanOrEqual(1); expect(screen.getByText(/Master complex topics/i)).toBeInTheDocument(); }); @@ -36,4 +36,13 @@ describe("Landing Page", () => { expect(screen.getByText("Linked Notes")).toBeInTheDocument(); expect(screen.getByText("Social Learning")).toBeInTheDocument(); }); + + it("renders 'How it works' section", () => { + renderLanding(); + + expect(screen.getByText("How it works")).toBeInTheDocument(); + expect(screen.getByText("Import")).toBeInTheDocument(); + expect(screen.getByText("Study")).toBeInTheDocument(); + expect(screen.getByText("Share")).toBeInTheDocument(); + }); }); diff --git a/web/src/pages/Landing.tsx b/web/src/pages/Landing.tsx index 225c4a9..a3e6951 100644 --- a/web/src/pages/Landing.tsx +++ b/web/src/pages/Landing.tsx @@ -1,88 +1,191 @@ +import { Footer } from "$components/layout/Footer"; import { A } from "@solidjs/router"; -import type { Component } from "solid-js"; +import type { Component, JSX } from "solid-js"; +import { For } from "solid-js"; +import { Motion } from "solid-motionone"; -const Feature: Component<{ title: string; desc: string }> = (props) => ( -
-

{props.title}

+const features = [{ + title: "Flashcards", + desc: "Built-in spaced repetition system (SRS) ensuring you review the right material at the right time.", + icon: , +}, { + title: "Linked Notes", + desc: "Connect concepts with bidirectional links. Build a knowledge graph that grows with your understanding.", + icon: , +}, { + title: "Lectures & Articles", + desc: "Import content directly. Highlight, annotate, and turn key insights into flashcards instantly.", + icon: , +}, { + title: "Social Learning", + desc: "Publish your decks, follow curators, and fork existing content to improve it for everyone.", + icon: , +}, { + title: "Local-First", + desc: "Your data lives on your device. Offline-first architecture with ATProto for decentralized sync.", + icon: , +}, { + title: "Open Source", + desc: "Validates knowledge, not proprietary locks. Inspect the code, extend the schema, own the platform.", + icon: ( + + + + ), +}]; + +const steps = [{ title: "Import", desc: "Create cards from articles, lectures, or write your own notes." }, { + title: "Study", + desc: "Review with spaced repetition — the right card at the right time.", +}, { title: "Share", desc: "Publish to the AT Protocol network and discover community content." }]; + +const Feature: Component<{ title: string; desc: string; icon: JSX.Element }> = (props) => ( +
+
{props.icon}
+

{props.title}

{props.desc}

); -const Landing: Component = () => { - return ( -
-
-
-
Malfestio
+const FloatingCard: Component<{ position: string; delay: number; children: JSX.Element }> = (props) => ( + + + {props.children} + + +); + +const StepCard: Component<{ step: number; title: string; desc: string }> = (props) => ( +
+
+ {props.step} +
+

{props.title}

+

{props.desc}

+
+); + +const Landing: Component = () => ( +
+
+
- -
-
+
+
+
+
+
+
+ +
-

+ Learning on
the AT Protocol. -

-

+ + Master complex topics with spaced repetition, linked notes, and active recall. Share your decks, notes, and discoveries with the community. -

- -
-
- -
-
- - - - - - +
-
-
- -
-
-

- © 2025 Stormlight Labs. Made with ⚡️ by - - Owais. - -

- + +
+
+ {(f) => } +
+
+
+
+
+
+

How it works

+
+ + {(s, i) => ( + <> + + {i() < steps.length - 1 && } + + )} +
-
-
- ); -}; + + +
+); export default Landing;