Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Gleam
uses: erlef/setup-beam@v1
- name: Setup Haskell
uses: haskell-actions/setup@v2
with:
otp-version: "27.1.2"
gleam-version: "1.11.0"
ghc-version: '9.12.2'
cabal-version: 'latest'

- name: Download dependencies
run: gleam deps download
- name: Build site generator
run: cabal update && cabal build

- name: Build site
run: gleam run
- name: Generate site
run: cabal run site-generator

- name: Setup Pages
uses: actions/configure-pages@v5
Expand Down
19 changes: 14 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
*.beam
*.ez
build
erl_crash.dump
# Haskell build artifacts
dist-newstyle/
*.hi
*.o
*.chi
*.chs.h
.hpc
.hsenv
cabal.project.local
cabal.project.local~

# Generated site
/dist/
/build/

# Environment
.env
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# Minimal CV site in Gleam
![Gleam](https://img.shields.io/badge/Gleam-1.0.0-blue.svg)
# Minimal CV site in Haskell
![Haskell](https://img.shields.io/badge/Haskell-9.12.2-purple.svg)

Basic profile site made needlessly complicated by writing it with Gleam.

Completely stole the set up from https://github.com/gleam-lang/website, kind of just made this as a mini way to learn Gleam.
Basic profile site made needlessly complicated by writing it with Haskell.

## Tech Stack

- **Gleam** - Static site generation
- **Haskell** - Static site generation
- **CSS** - Animations and styling
- **GitHub Actions** - CI/CD
- **GitHub Pages** - Hosting
Expand All @@ -16,22 +14,25 @@ Completely stole the set up from https://github.com/gleam-lang/website, kind of

### Prerequisites

- [Gleam](https://gleam.run/getting-started/installing/) >= 1.0.0
- [Erlang/OTP](https://www.erlang.org/downloads) >= 26.0
- [GHC](https://www.haskell.org/ghc/) >= 9.12.2
- [Cabal](https://www.haskell.org/cabal/) >= 3.0

### Commands

```bash
# Install dependencies
gleam deps download
# Update package index
cabal update

# Build the site generator
cabal build

# Build the site
gleam run
# Generate the site
cabal run site-generator

# Just open dist/index.html in your browser to view the site

# Clean build files
rm -rf dist build
rm -rf dist dist-newstyle
```

## Deployment
Expand Down
292 changes: 292 additions & 0 deletions app/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Control.Exception (catch, IOException)
import Data.Char (isSpace)
import Data.Maybe (fromMaybe)
import System.Directory (createDirectoryIfMissing)

main :: IO ()
main = buildSite `catch` handleError
where
handleError :: IOException -> IO ()
handleError err = do
putStrLn "❌ Error building site:"
print err

buildSite :: IO ()
buildSite = do
createDirectoryIfMissing True "dist"
writeIndexPage
writeCSS
putStrLn "✨ Site built successfully!"
putStrLn "📁 Files written to: dist/"

writeIndexPage :: IO ()
writeIndexPage = do
content <- readFile "content/index.djot"
let config = parseConfig content
html = wrapWithTemplate config "Max Harris - Software Engineer"
writeFile "dist/index.html" html

parseConfig :: String -> [(String, String)]
parseConfig content =
let processLine line = case break (== ':') line of
(key, ':':rest) -> Just (trim key, trim rest)
_ -> Nothing
in foldr (\line acc -> maybe acc (:acc) (processLine line)) [] (lines content)

trim :: String -> String
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace

getConfigValue :: [(String, String)] -> String -> String
getConfigValue config key = fromMaybe "" (lookup key config)

wrapWithTemplate :: [(String, String)] -> String -> String
wrapWithTemplate config title =
let name = getConfigValue config "name"
jobTitle = getConfigValue config "title"
about = getConfigValue config "about"
linkedinUrl = getConfigValue config "linkedin_url"
githubUrl = getConfigValue config "github_url"
email = getConfigValue config "email"
phone = getConfigValue config "phone"
in unlines
[ "<!DOCTYPE html>"
, "<html lang=\"en\">"
, "<head>"
, " <meta charset=\"UTF-8\">"
, " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
, " <title>" ++ title ++ "</title>"
, " <link rel=\"stylesheet\" href=\"style.css\">"
, " <meta name=\"description\" content=\"Max Harris - Software Engineer\">"
, "</head>"
, "<body>"
, " <div class=\"gradient-bg\">"
, " <div class=\"container\">"
, " <header class=\"hero\">"
, " <h1 class=\"gradient-text\">" ++ name ++ "</h1>"
, " <p class=\"lead\">" ++ jobTitle ++ "</p>"
, " </header>"
, " "
, " <main class=\"content\">"
, " <section class=\"about-section\">"
, " <div class=\"about-card\">"
, " <h2>About</h2>"
, " <p>" ++ about ++ "</p>"
, " </div>"
, " </section>"
, " "
, " <section class=\"contact-section\">"
, " <div class=\"contact-grid\">"
, " <a href=\"" ++ linkedinUrl ++ "\" class=\"contact-card\" target=\"_blank\" rel=\"noopener\">"
, " <div class=\"contact-icon\">💼</div>"
, " <h3>LinkedIn</h3>"
, " </a>"
, " "
, " <a href=\"" ++ githubUrl ++ "\" class=\"contact-card\" target=\"_blank\" rel=\"noopener\">"
, " <div class=\"contact-icon\">💻</div>"
, " <h3>GitHub</h3>"
, " </a>"
, " "
, " <a href=\"mailto:" ++ email ++ "\" class=\"contact-card\">"
, " <div class=\"contact-icon\">✉️</div>"
, " <h3>Email</h3>"
, " <p>" ++ email ++ "</p>"
, " </a>"
, " "
, " <a href=\"tel:" ++ phone ++ "\" class=\"contact-card\">"
, " <div class=\"contact-icon\">📱</div>"
, " <h3>Phone</h3>"
, " <p>" ++ phone ++ "</p>"
, " </a>"
, " </div>"
, " </section>"
, " </main>"
, " </div>"
, " </div>"
, "</body>"
, "</html>"
]

writeCSS :: IO ()
writeCSS = writeFile "dist/style.css" cssContent
where
cssContent = unlines
[ ":root {"
, " --faff-pink: #ffaff3;"
, " --unnamed-blue: #a6f0fc;"
, " --aged-plastic-yellow: #fffbe8;"
, " --underwater-blue: #292d3e;"
, " --charcoal: #2f2f2f;"
, " --white: #fefefc;"
, " --light-gray: #f5f5f5;"
, "}"
, ""
, "* {"
, " margin: 0;"
, " padding: 0;"
, " box-sizing: border-box;"
, "}"
, ""
, "body {"
, " font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;"
, " font-size: 16px;"
, " line-height: 1.6;"
, " color: var(--charcoal);"
, " min-height: 100vh;"
, "}"
, ""
, ".gradient-bg {"
, " background: linear-gradient(135deg, var(--faff-pink) 0%, var(--unnamed-blue) 50%, var(--aged-plastic-yellow) 100%);"
, " background-size: 200% 200%;"
, " animation: gradient-shift 15s ease infinite;"
, " min-height: 100vh;"
, " display: flex;"
, " align-items: center;"
, " padding: 2rem 0;"
, "}"
, ""
, "@keyframes gradient-shift {"
, " 0% { background-position: 0% 50%; }"
, " 50% { background-position: 100% 50%; }"
, " 100% { background-position: 0% 50%; }"
, "}"
, ""
, ".container {"
, " max-width: 800px;"
, " margin: 0 auto;"
, " padding: 0 2rem;"
, " width: 100%;"
, "}"
, ""
, ".hero {"
, " text-align: center;"
, " margin-bottom: 4rem;"
, "}"
, ""
, ".gradient-text {"
, " background: linear-gradient(45deg, var(--underwater-blue), var(--charcoal));"
, " -webkit-background-clip: text;"
, " background-clip: text;"
, " -webkit-text-fill-color: transparent;"
, " font-size: clamp(3rem, 8vw, 5rem);"
, " font-weight: 700;"
, " margin-bottom: 1rem;"
, "}"
, ""
, ".lead {"
, " font-size: clamp(1.5rem, 4vw, 2.5rem);"
, " font-weight: 300;"
, " color: var(--underwater-blue);"
, " margin-bottom: 0.5rem;"
, "}"
, ""
, ".subtitle {"
, " font-size: 1.25rem;"
, " color: var(--charcoal);"
, " opacity: 0.8;"
, "}"
, ""
, ""
, ".contact-grid {"
, " display: grid;"
, " grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));"
, " gap: 2rem;"
, " margin-bottom: 2rem;"
, "}"
, ""
, ".contact-card {"
, " background: rgba(255, 255, 255, 0.9);"
, " padding: 2rem;"
, " border-radius: 16px;"
, " text-decoration: none;"
, " color: inherit;"
, " transition: all 0.3s ease;"
, " text-align: center;"
, " backdrop-filter: blur(10px);"
, " border: 1px solid rgba(255, 255, 255, 0.2);"
, "}"
, ""
, ".contact-card:hover {"
, " transform: translateY(-4px);"
, " box-shadow: "
, " 0 20px 40px rgba(0, 0, 0, 0.1),"
, " 0 0 40px rgba(255, 175, 243, 0.2);"
, " background: rgba(255, 255, 255, 0.95);"
, "}"
, ""
, ".contact-icon {"
, " font-size: 3rem;"
, " margin-bottom: 1rem;"
, " display: block;"
, "}"
, ""
, ".contact-card h3 {"
, " font-size: 1.5rem;"
, " margin-bottom: 0.5rem;"
, " color: var(--underwater-blue);"
, "}"
, ""
, ".contact-card p {"
, " color: var(--charcoal);"
, " opacity: 0.8;"
, "}"
, ""
, ".about-section {"
, " display: flex;"
, " justify-content: center;"
, " margin-bottom: 3rem;"
, "}"
, ""
, ".about-card {"
, " background: rgba(255, 255, 255, 0.9);"
, " padding: 2.5rem;"
, " border-radius: 16px;"
, " backdrop-filter: blur(10px);"
, " border: 1px solid rgba(255, 255, 255, 0.2);"
, " max-width: 600px;"
, " text-align: center;"
, "}"
, ""
, ".about-card h2 {"
, " color: var(--underwater-blue);"
, " margin-bottom: 1rem;"
, " font-size: 2rem;"
, "}"
, ""
, ".about-card p {"
, " font-size: 1.1rem;"
, " line-height: 1.7;"
, "}"
, ""
, "@media (max-width: 768px) {"
, " .container {"
, " padding: 0 1rem;"
, " }"
, " "
, " .contact-grid {"
, " grid-template-columns: 1fr;"
, " gap: 1.5rem;"
, " }"
, " "
, " .contact-card, .about-card {"
, " padding: 1.5rem;"
, " }"
, "}"
, ""
, "@media (prefers-reduced-motion: reduce) {"
, " .gradient-bg {"
, " animation: none;"
, " }"
, " "
, " .contact-card {"
, " transition: none;"
, " }"
, " "
, " .contact-card:hover {"
, " transform: none;"
, " }"
, "}"
]
Loading