initial commit

This commit is contained in:
Daniel Kauss Serna 2026-01-26 13:19:50 +01:00
commit 6667a969b7
10 changed files with 1340 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
flake.lock
dist-newstyle

29
LICENSE Normal file
View file

@ -0,0 +1,29 @@
Copyright (c) 2026, Daniel Kauss
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

110
app/Main.hs Normal file
View file

@ -0,0 +1,110 @@
{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Types (status400)
import Data.ByteString.Lazy qualified as BL
import Data.Either (rights)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Time (addDays, getCurrentTime)
import Data.Time (MonthOfYear, Year, fromGregorian, toGregorian)
import Data.Time.LocalTime
import Ical
import Lucid (renderText)
import Network.HTTP.Simple
import Network.Wai.Middleware.Static
import Render
import Web.Scotty
import qualified Data.Text.Lazy as TL
import Text.Printf (printf)
getFromUrl :: String -> IO T.Text
getFromUrl url = do
r <- parseRequest url
response <- httpLBS r
let rBody = getResponseBody response
let rText = TE.decodeUtf8 (BL.toStrict rBody)
pure $ T.replace "\n " " " $ T.replace "\r\n" "\n" rText
main :: IO ()
main =
scotty
3456
( do
middleware $ staticPolicy (noDots >-> addBase "static")
get "/" $ do
linesArray <- liftIO $ getFromUrl "https://cdav.chipburners.club/public/main/"
timeNow <- liftIO $ getCurrentTime
tmz <- liftIO $ getCurrentTimeZone
let today = utcToLocalTime tmz timeNow
dayToday = localDay today
endD = addDays 30 dayToday
events = readCalendar linesArray
validEvents = take 5 $ getEventsFromTo (rights events) dayToday endD
-- TODO: errors
-- errors = lefts events
html $ renderText $ renderFrontpage validEvents
get "/monthView/" $ do
timeNow <- liftIO $ getCurrentTime
tmz <- liftIO $ getCurrentTimeZone
let today = utcToLocalTime tmz timeNow
dayToday = localDay today
(y, m, _) = toGregorian dayToday
redirect $ TL.pack $ printf "/monthView/%s/%s" (show y) (show m)
get "/monthView/:year/:month" $ do
year :: Year <- pathParam "year"
month :: MonthOfYear <- pathParam "month"
if (year <= 2000 || year >= 3000 || month < 1 || month > 12)
then do
status status400
text "Invalid date"
else do
linesArray <- liftIO $ getFromUrl "https://cdav.chipburners.club/public/main/"
timeNow <- liftIO $ getCurrentTime
tmz <- liftIO $ getCurrentTimeZone
let firstOfMonth = fromGregorian year month 1
start = addDays (-7) firstOfMonth
end = addDays (37) firstOfMonth
today = utcToLocalTime tmz timeNow
dayToday = localDay today
events = readCalendar linesArray
validEvents = getEventsFromTo (rights events) start end
groupedEvents = groupEvents validEvents
-- dayToday = localDay today
-- (y, m, _) = toGregorian dayToday
-- errors = lefts events
html $ renderText $ renderMonthview (year, month) dayToday groupedEvents
-- get "/greet/:name" $ do
-- name <- param "name"
-- "Hello, " <> name <> "! Hope you're enjoying Haskell."
)
-- pure finalHtml
-- let event = toEvent ["SUMMARY:Hello!", "Random Line", "DTSTART:20260123T140000", "DTEND:20260123T140000", "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,SA;COUNT=7"]
-- case event of
-- Right eve -> case parseDate "20260226T140000" of
-- Right c -> print $ expandEvent eve (localDay c)
-- _ -> print "Failed parse date"
-- _ -> print "failed event parse"
--
-- linesArray <- getFromUrl "https://cdav.chipburners.club/public/main/"
-- linesArray <- getFromUrl "http://www.upv.es/ical/3F60368113136708712FBB9C9243EDDC339D45EB9EAA9004F084E8DCF0F37A8F1AB9C2153EC8F12E1DFDFF671D7A52CE"
-- print (readCalendar linesArray)
-- end <- getCurrentTime
-- tmz <- getCurrentTimeZone
-- let today = utcToLocalTime tmz end
-- let endD = addDays 30 (localDay today)
-- let events = getNextEventsUntil (rights (readCalendar linesArray)) 10 endD
-- updateIndexHtml events
-- print
-- events

89
chipburners-club.cabal Normal file
View file

@ -0,0 +1,89 @@
cabal-version: 3.0
-- The cabal-version field refers to the version of the .cabal specification,
-- and can be different from the cabal-install (the tool) version and the
-- Cabal (the library) version you are using. As such, the Cabal (the library)
-- version used must be equal or greater than the version stated in this field.
-- Starting from the specification version 2.2, the cabal-version field must be
-- the first thing in the cabal file.
-- Initial package description 'chipburners-club' generated by
-- 'cabal init'. For further documentation, see:
-- http://haskell.org/cabal/users-guide/
--
-- The name of the package.
name: chipburners-club
-- The package version.
-- See the Haskell package versioning policy (PVP) for standards
-- guiding when and how versions should be incremented.
-- https://pvp.haskell.org
-- PVP summary: +-+------- breaking API changes
-- | | +----- non-breaking API additions
-- | | | +--- code changes with no API change
version: 0.1.0.0
-- A short (one-line) description of the package.
-- synopsis:
-- A longer description of the package.
-- description:
-- The license under which the package is released.
license: BSD-3-Clause
-- The file containing the license text.
license-file: LICENSE
-- The package author(s).
author: Daniel Kauss Serna
-- An email address to which users can send suggestions, bug reports, and patches.
maintainer: daniel.kauss.serna@gmail.com
-- A copyright notice.
-- copyright:
category: Web
build-type: Simple
-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
-- extra-doc-files: CHANGELOG.md
-- Extra source files to be distributed with the package, such as examples, or a tutorial module.
-- extra-source-files:
common warnings
ghc-options: -Wall
executable chipburners-club
-- Import common warning flags.
import: warnings
ghc-options: -threaded
-- .hs or .lhs file containing the Main module.
main-is: Main.hs
-- Modules included in this executable, other than Main.
other-modules: Ical, Render
-- LANGUAGE extensions used by modules in this package.
-- other-extensions:
extra-libraries: z zstd
-- Other library packages from which modules are imported.
build-depends:
base ^>=4.20.2.0,
http-conduit,
text,
bytestring,
containers,
scotty,
wai-middleware-static,
lucid,
http-types,
time
-- Directories containing source files.
hs-source-dirs: app, src
-- Base language which the package is written in.
default-language: GHC2024

34
flake.nix Normal file
View file

@ -0,0 +1,34 @@
{
description = "Chipburners home page";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
haskellApp = pkgs.haskellPackages.callCabal2nix "chipburners-club" ./. {
inherit (pkgs) zlib zstd;
};
in
{
packages.default = haskellApp;
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
ghc
cabal-install
haskell-language-server
zlib
zstd
pkg-config
];
shellHook = ''
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ pkgs.zlib pkgs.zstd ]}:$LD_LIBRARY_PATH"
'';
};
});
}

226
src/Ical.hs Normal file
View file

@ -0,0 +1,226 @@
{-# LANGUAGE OverloadedStrings #-}
module Ical (Event (..), readCalendar, getEventsFromTo, groupEvents) where
import Data.Function (on)
import Data.List (groupBy, sort, sortBy, sortOn)
import Data.Map qualified as M
import Data.Maybe (fromMaybe)
import Data.Ord (comparing)
import Data.Text qualified as T
import Data.Text.Read qualified as R
import Data.Time (Day, DayOfWeek (Monday), addDays, addGregorianMonthsClip, addGregorianYearsClip, defaultTimeLocale, parseTimeM, weekFirstDay)
import Data.Time.Calendar.WeekDate (toWeekDate)
import Data.Time.LocalTime
data Limit = Count Int | Until Day | NoLimit
deriving (Show, Eq)
data Frequency
= Daily
| Weekly {byDays :: [Int]}
| Monthly
| Yearly
| NoFreq
deriving (Show, Eq)
data RRule = RRule
{ freq :: Frequency,
interval :: Int,
limit :: Limit
}
deriving (Show, Eq)
data Event = Event
{ summary :: T.Text,
euid :: T.Text,
description :: T.Text,
location :: T.Text,
dtStart :: LocalTime,
dtEnd :: LocalTime,
rrule :: RRule
}
deriving (Show)
readCalendar :: T.Text -> [Either String Event]
readCalendar input =
let allBlocks = map T.lines $ T.splitOn "BEGIN:VEVENT" input
eventBlocks = drop 1 allBlocks -- Skip the header before the first VEVENT
in map toEvent eventBlocks
groupEvents :: [Event] -> [(Day, [Event])]
groupEvents events =
let sorted = sortOn dtStart events
grouped = groupBy ((==) `on` getDay) sorted
in map (\g -> (getDay (head g), g)) grouped
getDay :: Event -> Day
getDay = localDay . dtStart
getEventsFromTo :: [Event] -> Day -> Day -> [Event]
getEventsFromTo input startDay endDay =
-- let allEvents = foldl (\x y -> x ++ (expandEvent y endDay)) [] input
-- sorted = sortBy (comparing dtStart) allEvents
-- remove events that are after to avoid expanding, maybe not more efficent
let validEvents = filter ((< endDay) . getDay) input
allEvents = concatMap (\e -> expandEvent e endDay) validEvents
sorted = sortBy (comparing dtStart) allEvents
in filter ((> startDay) . getDay) sorted
streamDates :: Day -> RRule -> [Day]
streamDates start (RRule freq interval _) = case freq of
NoFreq -> [start]
Daily -> iterate (addDays $ toInteger interval) start
Monthly -> [addGregorianMonthsClip (toInteger $ n * interval) start | n <- [0 ..]]
Yearly -> [addGregorianYearsClip (toInteger $ n * interval) start | n <- [0 ..]]
Weekly days ->
let stepDays = toInteger (7 * interval)
targetOffsets =
if null days
then [dayOfWeekToInt start]
else sort days
mondayOfStart = weekFirstDay Monday start
weekMondays = iterate (addDays stepDays) mondayOfStart
in [ d
| monday <- weekMondays,
offset <- targetOffsets,
let d = addDays (toInteger offset) monday,
d >= start
]
applyLimit :: Limit -> [Day] -> [Day]
applyLimit lim dates = case lim of
NoLimit -> dates
Until u -> takeWhile (<= u) dates
Count c -> take c dates
expandEvent :: Event -> Day -> [Event]
expandEvent e@(Event _ _ _ _ start end rule) maxViewDate =
let startDate = localDay start
duration = diffLocalTime end start
infiniteStream = streamDates startDate rule
validRuleDates = applyLimit (limit rule) infiniteStream
-- TODO: I think its alway sorted so this works
visibleDates = takeWhile (<= maxViewDate) validRuleDates
in [ e
{ dtStart = newStart,
dtEnd = addLocalTime duration newStart,
rrule = rule {freq = NoFreq}
}
| d <- visibleDates,
let newStart = start {localDay = d}
]
dayOfWeekToInt :: Day -> Int
dayOfWeekToInt d =
let (_, _, dInt) = toWeekDate d
in dInt - 1
parseDate :: T.Text -> Either String LocalTime
parseDate input =
let str = T.unpack input
fmtDateTime = "%Y%m%dT%H%M%S"
fmtDate = "%Y%m%d"
in case parseTimeM True defaultTimeLocale fmtDateTime str of
Just t -> Right t
Nothing -> case parseTimeM True defaultTimeLocale fmtDate str of
Just t -> Right t
Nothing -> Left $ "Could not parse date: " ++ str
parseLine :: T.Text -> (T.Text, T.Text)
parseLine line =
let (key, value) = T.breakOn ":" line
in ( T.takeWhile (/= ';') key,
T.drop 1 value
)
daynameToInt :: T.Text -> Either String Int
daynameToInt day
| day == "MO" = Right 0
| day == "TU" = Right 1
| day == "WE" = Right 2
| day == "TH" = Right 3
| day == "FR" = Right 4
| day == "SA" = Right 5
| day == "SU" = Right 6
| otherwise = Left "Invalid week day"
-- TODO: clean this up
parseRRule :: T.Text -> Either String RRule
parseRRule "" = Right $ RRule NoFreq 0 NoLimit
parseRRule input = do
let params = M.fromList $ map parsePair $ T.splitOn ";" input
freq <- lookupParam "FREQ" params
let interval = fromMaybe 1 $ do
val <- M.lookup "INTERVAL" params
case R.decimal val of
Right (n, _) -> Just n
_ -> Nothing
limit <- case (M.lookup "COUNT" params, M.lookup "UNTIL" params) of
(Just c, _) -> do
(n, _) <- R.decimal c <?> "Invalid COUNT"
pure $ Count n
(_, Just d) -> do
date <- parseDate d <+?> "Invalid UNTIL date: "
pure $ Until $ localDay date
_ -> pure NoLimit
case freq of
"DAILY" -> Right $ RRule Daily interval limit
"WEEKLY" -> do
days <- parseByDay $ M.lookup "BYDAY" params
Right $ RRule (Weekly days) interval limit
"MONTHLY" -> Right $ RRule Monthly interval limit
"YEARLY" -> Right $ RRule Yearly interval limit
_ -> Left $ "Unknown FREQ: " ++ T.unpack freq
where
parsePair t = let (k, v) = T.breakOn "=" t in (k, T.drop 1 v)
lookupParam key m = case M.lookup key m of
Just v -> Right v
Nothing -> Left "Missing FREQ in RRULE"
parseByDay Nothing = Right []
parseByDay (Just txt) = traverse daynameToInt $ T.splitOn "," txt
toEvent :: [T.Text] -> Either String Event
toEvent input = do
let params = M.fromList $ map parseLine input
let require p = maybe (Left $ "Missing property: " ++ T.unpack p) Right $ M.lookup p params
let optional opt p = M.findWithDefault opt p params
summText <- require "SUMMARY"
uid <- require "UID"
startText <- require "DTSTART"
endText <- require "DTEND"
let loc = optional "No location" "LOCATION"
let descriptionText = optional "No description" "DESCRIPTION"
let rruleTxt = M.lookup "RRULE" params
rrule <- case rruleTxt of
Just txt -> parseRRule txt
Nothing -> Right $ RRule NoFreq 0 NoLimit
startDate <- parseDate startText <+?> "Error reading start Date: "
endDate <- parseDate endText <+?> "Error reading end Date: "
pure $ Event summText uid descriptionText loc startDate endDate rrule
-- This is very nice, replace Left with a different Left
(<?>) :: Either e a -> String -> Either String a
Left _ <?> msg = Left msg
Right x <?> _ = Right x
-- Append prev Left to the end
(<+?>) :: Either String a -> String -> Either String a
Left x <+?> msg = Left $ msg ++ x
Right x <+?> _ = Right x

224
src/Render.hs Normal file
View file

@ -0,0 +1,224 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Render (renderFrontpage, renderMonthview) where
import Control.Monad (forM_)
import Data.Maybe (fromMaybe)
import Data.Text qualified as T
import Data.Time (Day, MonthOfYear, Year, addDays, addGregorianMonthsClip, dayOfWeek, defaultTimeLocale, formatTime, fromGregorian, toGregorian)
import Ical
import Lucid
import Text.Printf (printf)
renderHead :: Html ()
renderHead = do
doctype_
head_ $ do
meta_ [charset_ "UTF-8"]
meta_
[ name_ "viewport",
content_ "width=device-width, initial-scale=1.0"
]
meta_
[ name_ "description",
content_ "Chipburners home page"
]
title_ "Chipburners"
link_ [rel_ "stylesheet", href_ "/style.css"]
renderHeader :: Int -> Html ()
renderHeader active =
header_ [class_ "site-header"] $ do
nav_ [] $ do
ul_ [class_ "nav-list"] $ do
li_ $ a_ [href_ "/", getActive 0] "Chip"
li_ $ a_ [href_ "/monthView", getActive 1] "Eventos"
li_ $ a_ [href_ "/wiki", getActive 2] "Wiki"
li_ $ a_ [href_ "/contact", getActive 3] "Contacto"
where
getActive cur = if cur == active then class_ "active" else class_ "inactive"
renderImage :: T.Text -> T.Text -> T.Text -> Html ()
renderImage url caption alt =
figure_ $ do
img_
[ src_ url,
alt_ alt,
loading_ "lazy"
]
figcaption_ $ toHtml caption
renderSideEvent :: Event -> Html ()
renderSideEvent e =
li_ $ do
span_ [class_ "event-time"] $
toHtml (formatTime defaultTimeLocale "%H:%M" (dtStart e))
span_ [class_ "event-title"] $ do
let uid = euid e
a_ [href_ ("/monthView#" <> uid), class_ "event-link"] $ toHtml (summary e)
renderDayGroup :: (Day, [Event]) -> Html ()
renderDayGroup (day, events) =
li_ [class_ "event-group"] $ do
time_
[ datetime_ $ T.pack (formatTime defaultTimeLocale "%Y-%m-%d" day),
class_ "date-header"
]
$ toHtml
$ T.pack (formatTime defaultTimeLocale "%A %d.%m.%y" day)
ul_ [class_ "daily-events"] $
mapM_ renderSideEvent events
renderEventList :: [Event] -> Html ()
-- mapM_ only cares about the monad I think? So it throws away the () part
-- mapM applies the monad to everz element of the list and returns monad with a list?
renderEventList events = mapM_ renderDayGroup (groupEvents events)
renderFrontpage :: [Event] -> Html ()
renderFrontpage events = do
doctype_
html_ [lang_ "es"] $ do
renderHead
body_ $ do
renderHeader 0
div_ [class_ "wrapper"] $ do
div_ [class_ "layout"] $ do
main_ [id_ "main-content", class_ "content"] $ do
section_ [class_ "intro"] $ do
h1_ "Chipburners_"
p_ [class_ "lead"] $
"Frase motivadora aqui"
p_ $ do
"Buenos dias, "
br_ []
"Somos un grupo d personas y entidades interesados en infromatica y \
\las technologias relacionadas. "
br_ []
"Este hackerspace ha sido creado como manera d juntar gente con intereses \
\similares y compartir conocimientos. "
br_ []
"Contactanos en "
a_ [href_ "/contact"] "signal"
"!"
section_ [class_ "image-gallery"] $ do
renderImage "example.com" "Imagen sobre algo" "Imagen interesante"
aside_ [class_ "sidebar"] $ do
section_ [class_ "events-panel"] $ do
h2_ "Eventos"
renderEventList events
chunksOf :: Int -> [a] -> [[a]]
chunksOf _ [] = []
chunksOf n xs =
let (ys, zs) = splitAt n xs
in ys : chunksOf n zs
padZero :: Int -> String
padZero n = if n < 10 then "0" ++ show n else show n
renderCalendarEvent :: Event -> Html ()
renderCalendarEvent e = do
let uid = euid e
li_ [class_ "event-item"] $ do
-- (toHtml $ summary e)
a_ [href_ ("#" <> uid), class_ "event-link"] $ toHtml (summary e)
div_ [class_ "event-popup", id_ uid] $ do
h1_ [] $ do
(toHtml $ summary e)
a_
[ href_ ("#None"),
class_ "close-btn"
]
"×"
p_ [class_ "lead"] $
toHtml $
T.pack $
printf
"%s -> %s - %s"
(formatTime defaultTimeLocale "%A %d.%m.%y" $ dtStart e)
(formatTime defaultTimeLocale "%H:%M" $ dtStart e)
(formatTime defaultTimeLocale "%H:%M" $ dtEnd e)
p_ [] $
toHtml $
description e
renderMonthview :: (Year, MonthOfYear) -> Day -> [(Day, [Event])] -> Html ()
renderMonthview (year, month) today groupedEvents = do
let firstOfMonth = fromGregorian year month 1
wd = dayOfWeek firstOfMonth
daysToSubtract = fromEnum wd - 1
startOfGrid = addDays (fromIntegral (-daysToSubtract)) firstOfMonth
gridDays = [addDays i startOfGrid | i <- [0 .. 41]]
weeks = chunksOf 7 gridDays
prevMonthDay = addGregorianMonthsClip (-1) firstOfMonth
nextMonthDay = addGregorianMonthsClip 1 firstOfMonth
(py, pm, _) = toGregorian prevMonthDay
(cy, cm, _) = toGregorian today
(ny, nm, _) = toGregorian nextMonthDay
mkLink y m label =
a_
[ href_ $ T.pack $ printf "/monthView/%s/%s" (show y) (padZero m),
class_ "nav-link"
]
label
renderHead
renderHeader 1
div_ [class_ "calendar-wrapper"] $ do
header_ [class_ "calendar-header"] $ do
h1_ [class_ "view-title"] "Vista mensual"
nav_ [class_ "calendar-nav"] $ do
mkLink py pm "« Mes pasado"
mkLink cy cm "Este mes"
mkLink ny nm "Mes siguiente »"
h2_ [class_ "month-name"] $
toHtml $
formatTime defaultTimeLocale "%B %Y" $
fromGregorian year month 1
table_ [class_ "calendar-table"] $ do
thead_ $ do
tr_ $ do
th_ "Lunes"
th_ "Martes"
th_ "Miercoles"
th_ "Jueves"
th_ "Viernes"
th_ "Sabado"
th_ "Domingo"
tbody_ $ do
forM_ weeks $ \week -> do
tr_ $ do
forM_ week $ \d -> do
let (_, dM, dD) = toGregorian d
isCurrentMonth = dM == month
isToday = d == today
baseClasses = []
monthClass = if isCurrentMonth then [] else ["other-month"]
todayClass = if isToday then ["current-day"] else []
finalClass = T.intercalate " " (baseClasses ++ monthClass ++ todayClass)
-- TODO: lookup very slow :(
dayEvents = fromMaybe [] (lookup d groupedEvents)
-- cool
td_ ([class_ finalClass | not (T.null finalClass)]) $ do
span_ [class_ "day-number"] (toHtml $ show dD)
if null dayEvents
then return ()
else ul_ [class_ "event-list"] $ do
forM_ dayEvents $ \e ->
renderCalendarEvent e

BIN
static/Geo-Regular.woff2 Normal file

Binary file not shown.

157
static/monthView.html Normal file
View file

@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Month View Calendar Template</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="calendar-wrapper">
<header class="calendar-header">
<h1 class="view-title">Month View</h1>
<nav class="calendar-nav">
<a href="?month=09&year=2023" class="nav-link">&laquo; Prev Month</a>
<a href="?month=10&year=2023" class="nav-link">This Month</a>
<a href="?month=11&year=2023" class="nav-link">Next Month &raquo;</a>
</nav>
<h2 class="month-name">October 2023</h2>
</header>
<table class="calendar-table">
<thead>
<tr>
<th>Mon</th>
<th>Tue</th>
<th>Wed</th>
<th>Thu</th>
<th>Fri</th>
<th>Sat</th>
<th>Sun</th>
</tr>
</thead>
<tbody>
<tr>
<td class="other-month">
<span class="day-number">25</span>
</td>
<td class="other-month">
<span class="day-number">26</span>
</td>
<td class="other-month">
<span class="day-number">27</span>
</td>
<td class="other-month">
<span class="day-number">28</span>
</td>
<td class="other-month">
<span class="day-number">29</span>
</td>
<td class="other-month">
<span class="day-number">30</span>
</td>
<td>
<span class="day-number">1</span>
<ul class="event-list">
<li class="event-item">Rent Due</li>
</ul>
</td>
</tr>
<tr>
<td>
<span class="day-number">2</span>
</td>
<td>
<span class="day-number">3</span>
<ul class="event-list">
<li class="event-item">Team Meeting (10am)</li>
<li class="event-item urgent">Project Deadline</li>
</ul>
</td>
<td>
<span class="day-number">4</span>
</td>
<td>
<span class="day-number">5</span>
</td>
<td class="current-day">
<span class="day-number">6</span>
<ul class="event-list">
<li class="event-item personal">Dentist Appt</li>
</ul>
</td>
<td>
<span class="day-number">7</span>
<ul class="event-list">
<li class="event-item">Grocery shopping</li>
</ul>
</td>
<td>
<span class="day-number">8</span>
</td>
</tr>
<tr>
<td><span class="day-number">9</span></td>
<td><span class="day-number">10</span></td>
<td><span class="day-number">11</span></td>
<td><span class="day-number">12</span></td>
<td><span class="day-number">13</span></td>
<td>
<span class="day-number">14</span>
<ul class="event-list">
<li class="event-item">Birthday Party</li>
</ul>
</td>
<td><span class="day-number">15</span></td>
</tr>
<tr>
<td><span class="day-number">16</span></td>
<td><span class="day-number">17</span></td>
<td>
<span class="day-number">18</span>
<ul class="event-list">
<li class="event-item urgent">Server Maintenance</li>
</ul>
</td>
<td><span class="day-number">19</span></td>
<td><span class="day-number">20</span></td>
<td><span class="day-number">21</span></td>
<td><span class="day-number">22</span></td>
</tr>
<tr>
<td><span class="day-number">23</span></td>
<td><span class="day-number">24</span></td>
<td><span class="day-number">25</span></td>
<td><span class="day-number">26</span></td>
<td><span class="day-number">27</span></td>
<td><span class="day-number">28</span></td>
<td><span class="day-number">29</span></td>
</tr>
<tr>
<td><span class="day-number">30</span></td>
<td>
<span class="day-number">31</span>
<ul class="event-list">
<li class="event-item">Halloween Party</li>
</ul>
</td>
<td class="other-month"><span class="day-number">1</span></td>
<td class="other-month"><span class="day-number">2</span></td>
<td class="other-month"><span class="day-number">3</span></td>
<td class="other-month"><span class="day-number">4</span></td>
<td class="other-month"><span class="day-number">5</span></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

469
static/style.css Normal file
View file

@ -0,0 +1,469 @@
/*
:root {
--black-base: #141414;
--black-surface: #1f1f1f;
--black-elevated: #10101a;
--black4: #0f0d0c;
--accent-red: #e88a80;
--accent-orange: #d3a07a;
--accent-cream: #eed9bf;
--text-main: #d7d4f5;
--ui-ice: #e3e8f5;
--ui-blue: #afb8e6;
--grey-muted: #808080;
--grid-color: rgba(207, 216, 230, 0.1);
--font-main: "Geo", "Courier New", monospace;
--spacing-md: 2rem;
--spacing-sm: 1rem;
}
*/
:root {
--black-base: #0d0f12;
--black-surface: #161a1f;
--black-elevated: #21262d;
--accent-red: #e88a80;
--accent-orange: #d3a07a;
--accent-cream: #eed9bf;
--text-main: #c9d1d9;
--ui-blue: #8fa1d0;
--ui-ice: #e3e8f5;
--grey-muted: #6e7681;
--grid-color: rgba(143, 161, 208, 0.08);
--highlight-soft: rgba(240, 240, 255, 0.07);
--highlight-hard: rgba(240, 240, 255, 0.13);
--highlight-red: rgba(232, 138, 128, 0.05);
--font-main: "Geo", "Courier New", monospace;
--spacing-md: 2rem;
--spacing-sm: 1rem;
}
@font-face {
font-family: Geo;
src: url("/Geo-Regular.woff2");
/* url of the font */
}
/* idk this is good? */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
background-color: var(--black-base);
/* grid, idk */
/* background-image: */
/* linear-gradient(var(--grid-color) 1px, transparent 1px), */
/* linear-gradient(90deg, var(--grid-color) 1px, transparent 1px); */
/* background-size: 30px 30px; */
color: var(--accent-cream);
font-family: var(--font-main);
font-weight: 400;
font-size: 25px;
line-height: 1.2;
margin: 0;
padding-bottom: 50px;
}
img {
max-width: 100%;
height: auto;
display: block;
border: 1px solid var(--grey-muted);
}
h1,
h2,
h3 {
margin-top: 0;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 1px;
}
a {
color: var(--ui-ice);
text-decoration: none;
border-bottom: 1px dashed transparent;
transition: color 0.2s, border-color 0.2s;
}
a:hover,
a:focus {
color: var(--accent-orange);
border-bottom-color: var(--accent-orange);
outline: none;
}
.site-header {
padding: var(--spacing-sm);
margin-bottom: var(--spacing-md);
background: var(--black-surface);
border-image: linear-gradient(to right,
var(--accent-red), var(--accent-orange), var(--text-main), var(--ui-ice), var(--ui-blue)) 1;
border-bottom: 1px solid var(--grey-muted);
}
.nav-list {
padding: 0;
margin: 0 auto;
max-width: 600px;
display: flex;
justify-content: center;
gap: 30px;
list-style-type: none;
flex-wrap: wrap;
/* Idk if his is good */
}
.nav-list a {
font-size: 1.2rem;
padding: 5px 10px;
}
/* Maybe change back to single line */
.nav-list a:hover,
.nav-list a.active {
border-bottom: 2px solid var(--text-main);
}
.wrapper {
max-width: 1100px;
margin: 0 auto;
padding: 0 var(--spacing-sm);
}
.layout {
display: flex;
gap: 3rem;
align-items: flex-start;
}
.content {
/* background-color: var(--black4); */
flex: 2.5;
min-width: 0;
border-left: 2px solid var(--accent-red);
padding-left: var(--spacing-md);
padding-right: var(--spacing-md);
padding-bottom: var(--spacing-md);
}
.content h1 {
color: var(--accent-red);
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.lead {
font-size: 1.2rem;
color: var(--accent-orange);
margin-bottom: var(--spacing-md);
}
figure {
margin: 0;
background: var(--black-soft);
padding: 10px;
border: 1px solid var(--grey-muted);
}
figcaption {
font-size: 0.9rem;
color: var(--grey-muted);
margin-top: 5px;
text-align: right;
}
.sidebar {
flex: 1;
/* background: var(--black3); */
border-left: 1px solid var(--ui-blue);
padding: var(--spacing-sm);
/* Idk maybe this is bad*/
position: sticky;
top: 20px;
}
.events-panel h2 {
color: var(--ui-blue);
font-size: 1.5rem;
border-bottom: 1px solid var(--ui-blue);
padding-bottom: 10px;
margin-bottom: 15px;
}
.event-list {
list-style: none;
padding: 0;
margin: 0;
}
.event-group {
margin-bottom: 20px;
}
.date-header {
display: block;
color: var(--text-main);
font-weight: bold;
margin-bottom: 8px;
text-transform: uppercase;
}
.daily-events {
list-style: none;
padding-left: 0;
margin: 0;
border-left: 1px dashed var(--grey-muted);
}
.daily-events li {
display: flex;
/* justify-content: space-between; */
padding: 5px 10px;
color: var(--ui-ice);
transition: background-color 0.1s;
}
.daily-events li:hover {
background-color: var(--highlight-soft);
}
.event-time {
color: var(--grey-muted);
font-size: 0.9em;
}
.event-title {
/* text-align: right; */
text-align: left;
padding-left: 12px;
}
@media (max-width: 800px) {
.layout {
flex-direction: column;
gap: var(--spacing-md);
}
.content {
border-left: none;
border-top: 2px solid var(--accent-red);
padding: var(--spacing-sm);
}
.sidebar {
width: 100%;
position: static;
}
.site-header {
margin-bottom: 1rem;
}
}
/* Calendar ,TODO move to another file? */
.calendar-wrapper {
max-width: 1200px;
margin: 25px auto;
background: var(--black-surface);
padding: 25px;
border: 1px solid var(--grey-muted);
}
.calendar-header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid var(--accent-red);
border-image: linear-gradient(to right, var(--accent-red), var(--accent-orange), var(--text-main), var(--ui-ice), var(--ui-blue)) 1;
padding-bottom: 20px;
}
.view-title {
font-size: 1rem;
color: var(--text-main);
margin: 0 0 5px 0;
text-transform: uppercase;
letter-spacing: 2px;
}
.month-name {
font-size: 3rem;
margin: 5px 0;
color: var(--accent-red);
text-transform: uppercase;
}
.calendar-nav {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
}
.nav-link {
text-decoration: none;
color: var(--ui-ice);
font-weight: 400;
padding: 8px 20px;
border: 1px dashed var(--ui-ice);
transition: all 0.2s ease;
text-transform: uppercase;
font-size: 0.9rem;
}
.nav-link:hover {
background-color: var(--accent-orange);
color: var(--black-base);
border-style: solid;
border-color: var(--accent-orange);
}
.calendar-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
background-color: var(--black4);
}
.calendar-table th {
background-color: var(--black-elevated);
color: var(--ui-blue);
padding: 15px;
text-align: center;
border: 1px solid var(--grey-muted);
font-weight: 400;
text-transform: uppercase;
font-size: 0.8rem;
}
.calendar-table td {
border: 1px solid var(--grid-color);
height: 140px;
vertical-align: top;
padding: 10px;
background-color: transparent;
transition: background-color 0.1s;
}
.calendar-table td:hover {
background-color: var(--highlight-soft);
}
.calendar-table td.other-month {
background-color: var(--black-base);
color: var(--grey-muted);
}
.day-number {
font-weight: 400;
font-size: 1.3rem;
margin-bottom: 10px;
display: block;
color: var(--text-main);
}
.current-day {
background-color: var(--highlight-red) !important;
}
.current-day .day-number {
color: var(--accent-red);
text-decoration: underline;
text-underline-offset: 4px;
}
.event-item {
background-color: var(--black-surface);
color: var(--ui-blue);
padding: 2px 3px;
margin-bottom: 6px;
border: 1px solid var(--ui-blue);
border-left: 2px solid var(--ui-blue);
font-size: 1.3rem;
/*! white-space: nowrap; */
overflow: hidden;
text-overflow: None;
transition: background-color 0.1s;
}
.event-item a:hover {
text-decoration: none;
}
.event-item:hover {
/* kinda cool, maybe later more*/
/* transform: translateX(3px); */
/* this breaks the popup :( */
/* filter: brightness(1.2); */
background-color: var(--highlight-hard);
}
.event-popup {
display: block;
opacity: 0;
visibility: hidden;
position: fixed;
top: 10%;
left: 10%;
background: var(--black-base);
width: 80%;
height: 80%;
padding: 20px;
z-index: 1;
border: 3px solid;
border-image: linear-gradient(to right, var(--accent-red), var(--accent-orange), var(--text-main), var(--ui-ice), var(--ui-blue)) 1;
transition: opacity 0.1;
}
.event-popup h1 {
border-bottom: 1px solid var(--text-main);
display: flex;
justify-content: space-between;
}
.event-popup:target {
visibility: visible;
opacity: 1;
}
/* .show .event-popup { */
/* visibility: visible; */
/* -webkit-animation: fadeIn 0.1s; */
/* animation: fadeIn 0.1s */
/* } */
/* Add animation (fade in the popup) */
@-webkit-keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}