Compare commits

...

42 Commits

Author SHA1 Message Date
efim
0af0129054 feat(18): not really working docker images 2024-03-15 06:49:46 +00:00
efim
8ff2ec3766 feat(18): displaying + for positive month comparison
wrapping + into span inline, to make it behave like part of
surrounding text in a div, somewhat ok
2024-03-14 14:10:23 +00:00
efim
843c71946f feat(18): desktop styling close enough 2024-03-14 13:51:32 +00:00
efim
51e3629fa5 feat(18): resize without transform, hover states
using transform influences text size, and size of rounded corners

current way hardcodes 8rem for the columns,
maybe i'll pass it as parameter when start doing desktop, i don't know
feels like not a good choice overall, but yeah
2024-03-14 13:04:30 +00:00
efim
ddc003eb13 feat(18): random input data 2024-03-13 17:23:32 +00:00
efim
e81a22dd10 feat(18): adding dynamic data 2024-03-13 17:08:03 +00:00
efim
269cb2967c feat(18): total this month part styled 2024-03-13 16:42:03 +00:00
efim
6b764ae61b fix(18): prettifying styles 2024-03-13 16:07:32 +00:00
efim
44c89e6559 feat(18): google fonts fix
maybe they stopped working without tracking?
2024-03-13 15:57:42 +00:00
efim
a541a2dac2 feat(18): spending bars curDay color 2024-03-13 15:25:25 +00:00
efim
ff7baa0a24 feat(18): initial spending columns component
tailwind doesn't seem to allow scale-y-[var(--the-var)]
maybe that's just the scale thing

overall, still setting style variables, this time with css blocks,
i suppose that's ok
2024-03-13 15:15:10 +00:00
efim
623d05da91 feat(18): basic templates 2024-03-13 12:58:26 +00:00
efim
8601288230 feat(18): colors and fonts to tailwind 2024-03-12 09:00:57 +00:00
efim
fa11926642 init: exercise expenses chart resources 2024-03-12 07:44:13 +00:00
efim
9c8ec7fb0d Add '18-expenses-chart/' from commit '78c9bd1d614c8bdd25a3e1d23bd0a39bb01e65f1'
git-subtree-dir: 18-expenses-chart
git-subtree-mainline: e13fa186e1
git-subtree-split: 78c9bd1d61
2024-03-12 07:09:35 +00:00
efim
78c9bd1d61 refactor: moving templates into dir
routes probably can live in main.go for simpler exercises
2024-03-12 05:46:57 +00:00
efim
9c19bd7b6b feat: initialized tailwind
basic conig targetting templ
templ html page with import
Makefile job that generates for the run
2024-03-11 10:45:47 +00:00
efim
de2fd2bdc0 feat: Makefile for local development
would be even more useful when i'll get tailwind and stuff
2024-03-11 07:32:28 +00:00
efim
81235e3ce6 feat: enabling templ 2024-03-11 04:44:55 +00:00
efim
4f7b2fcd17 init 2024-03-10 19:24:30 +00:00
efim
e13fa186e1 feat: preserving name \ region on active search
by using post - all inputs of same form are sent
so in active search scenario changing region also sends current name,
or changing name also sends current region

had to make additional post endpoint,
but with cask i can directly use form-value as function argument
and can reuse the code, yay
2023-10-11 03:22:31 +00:00
efim
f238940622 docs: readme for first Go exercise 2023-10-04 11:52:18 +00:00
efim
7f4b8cab8a feat: generating tailwind out.css in nix build 2023-10-04 11:28:28 +00:00
efim
b0dd8cded1 feat: docker image for results component 2023-10-04 10:39:33 +00:00
efim
f48d958a2c feat: passing of the port as arg 2023-10-04 09:25:13 +00:00
efim
2cb3cc7c35 feat: split results into insertable component
still not fully sure how this all operates, but allright.
2023-10-04 09:13:12 +00:00
efim
f4ab1ac7ec feat: other fields taken from template data 2023-10-04 08:38:31 +00:00
efim
2769f5e8dc feat: desktop styling 2023-10-04 08:28:02 +00:00
efim
d7dce88751 feat: icons added via <img> by static path 2023-10-04 06:23:34 +00:00
efim
b036002ca8 feat: repeating category in template 2023-10-04 06:03:04 +00:00
efim
70ab1e59c4 feat: button styled 2023-10-04 05:20:55 +00:00
efim
133fa0df2b feat: colors for items via css variable 2023-10-04 05:16:52 +00:00
efim
58ca4ecafa feat: upper part styled 2023-10-03 18:13:16 +00:00
efim
843e55841b fix: fontFamily override name 2023-10-03 17:56:40 +00:00
efim
cfe3994bc9 feat: style guilde fonts enabled 2023-10-03 16:48:45 +00:00
efim
8b95d963fe feat: enabled tailwindcss
and have single watcher command to rebuild out.css and restart the
server:
wgo -verbose -file .go -file .gohtml echo reloading :: bash -c 'tailwindcss -i ./input.css -o public/out.css' :: go run main.go
2023-10-03 15:06:18 +00:00
efim
83d8cd07d2 init: adding frontendmentor task resources 2023-10-03 14:27:38 +00:00
efim
6b323ba746 feat: embedding templates as well
maybe it's possible to somehow limit file server to a directory
2023-10-03 14:12:20 +00:00
efim
e834ff06ab feat: responding with a basic template 2023-10-03 13:52:01 +00:00
efim
f30d9dad94 init: 17 - simple page with go ssr 2023-10-03 10:15:07 +00:00
efim
ee914c8014 feat: active search on main page
over english and native nave, nice
also putting url into history, and also setting up the load anchor
2023-09-28 04:33:41 +00:00
efim
b8d0dc96fd docs: readme for exercise 16 2023-09-27 06:59:43 +00:00
62 changed files with 1523 additions and 51 deletions

0
.go/pkg/mod/cache/lock vendored Normal file
View File

View File

@@ -0,0 +1,136 @@
* Frontend Mentor - REST Countries API with color theme switcher solution
:PROPERTIES:
:CUSTOM_ID: frontend-mentor---rest-countries-api-with-color-theme-switcher-solution
:END:
This is a solution to the
[[https://www.frontendmentor.io/challenges/rest-countries-api-with-color-theme-switcher-5cacc469fec04111f7b848ca][REST
Countries API with color theme switcher challenge on Frontend Mentor]].
Frontend Mentor challenges help you improve your coding skills by
building realistic projects.
** Overview
:PROPERTIES:
:CUSTOM_ID: overview
:END:
*** The challenge
:PROPERTIES:
:CUSTOM_ID: the-challenge
:END:
Users should be able to:
- See all countries from the API on the homepage
- Search for a country using an =input= field
- Filter countries by region
- Click on a country to see more detailed information on a separate page
- Click through to the border countries on the detail page
- Toggle the color scheme between light and dark mode /(optional)/
*** Screenshot
:PROPERTIES:
:CUSTOM_ID: screenshot
:END:
[[mobile-main.png]]
[[mobile-single.png]]
[[desktop-main.png]]
[[desktop-single.png]]
*** Links
:PROPERTIES:
:CUSTOM_ID: links
:END:
- Solution URL: https://github.com/efim/Learning-HTMX/tree/master/16-countries-page-from-api
- Live Site URL: https://efim-frontentmentor-countries-page.onrender.com/
** My process
:PROPERTIES:
:CUSTOM_ID: my-process
:END:
*** Built with
:PROPERTIES:
:CUSTOM_ID: built-with
:END:
- Scala Server Side Rendering
- Htmx
- TailwindCSS
- Flexbox
- CSS Grid
- Mobile-first workflow
*** What I learned
:PROPERTIES:
:CUSTOM_ID: what-i-learned
:END:
**** polling download for 'infinite scroll'
https://htmx.org/examples/infinite-scroll/
I've done almost same thing as example,
but i'm appending additional empty div, that inserts responses instead of itself
this way i have one template fragment with repeater,
and don't have to add hx attributes to the last one,
i just have separate fragment that i append to response which will trigger new
load on enter view.
**** having html 5 native autocomplete with <datalist> tag
just server side rendering it with all country names is yay
**** inserting thymeleaf templates by css selector
https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#appendix-c-markup-selector-syntax
this way i can insert <script> tag from index page, without declaring it as a fragment,
because i'll want is static anyway.
and can share 'dark mode' js code between pages
**** building docker image
First off: using nix docker tools, and sbt assembly to create 'uber jar'
now 'nix build .#countries-page-image' builds an image and symlinks it to ./result
$ docker load < result
loads the image
and it can be started
$ docker image list
$ docker run -d -p 9090:8080 <image-id>
**** deploying to render.com
need to auth the docker cli, then tag an image with my repository name
$ docker login
$ docker tag 141 efim1234/rock-paper-scissors:latest
$ podman push localhost/efim1234/countries-page:latest docker.io/efim1234/countries-page:latest
and now render.com, when creating "new service"
can find "efim1234/countries-page:latest" as public image and use it
**** using browser history
with either server side
HX-Push -> "some-url"
or
hx-push-url="true" in the html side
so that url is put into address bar,
and browser save the body into history
and now my website has forward and backward navigation
only bug - the 'filter by region' for some reason doesn't maintain the selected value
*** Continued development
:PROPERTIES:
:CUSTOM_ID: continued-development
:END:
In future project i'll probably want to lean and use go.
For smaller binaries of the server, using PocketBase as embedded backend functions,
and maybe having a better chances of just doing some real world small projects.
*** Useful resources
:PROPERTIES:
:CUSTOM_ID: useful-resources
:END:
- htmx documentation and examples
https://htmx.org/docs/#introduction
- thymeleaf documentation
https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#introducing-thymeleaf
- TailwindCSS documentation
https://tailwindcss.com/docs/installation
- heroicons: MIT licensed svg icons
https://github.com/tailwindlabs/heroicons
- render.com for providing free hosting for services from docker images

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -61,10 +61,10 @@
hx-get="/country"
hx-target="body"
hx-push-url="true"
class="md:w-1/4 md:min-w-max"
class="contents"
>
<div
class="flex flex-row items-center bg-white shadow-md h-14 md:h-16 rounded-lg md:w-full dark:bg-dark-blue"
class="md:w-1/4 md:min-w-max flex flex-row items-center bg-white shadow-md h-14 md:h-16 rounded-lg dark:bg-dark-blue"
>
<!-- <p class="w-16 grid place-content-center">IC</p> -->
<svg
@@ -87,6 +87,9 @@
name="countryName"
list="country.nameViews-list"
placeholder="Search for a country..."
hx-post="/countries-cards"
hx-target="#countries-main-list"
hx-trigger="keyup changed delay:500ms"
/>
</div>
<datalist id="country.nameViews-list">
@@ -96,12 +99,10 @@
value=""
></option>
</datalist>
</form>
<form>
<select
name="region"
class="block w-62 mt-1 h-14 md:h-16 w-64 bg-white shadow-lg rounded-md shadow-sm focus:outline-none px-7 dark:bg-dark-blue"
hx-get="/countries-cards"
hx-post="/countries-cards"
hx-target="#countries-main-list"
>
<option
@@ -180,7 +181,7 @@
th:if="${nextPage != -1}"
id="next-page-anchor"
hx-trigger="revealed"
th:hx-get="@{~/countries-cards(region=${selectedRegion},page=${nextPage})}"
th:hx-get="@{~/countries-cards(region=${selectedRegion},page=${nextPage},countryName=${countryName})}"
hx-swap="outerHTML"
></div>

View File

@@ -33,15 +33,15 @@ case class Routes(countries: List[Country])(implicit
private val pageSize = 12
@cask.get("/")
def indexPage(region: Option[String] = None, page: Int = 0) = {
def indexPage(
region: Option[String] = None,
page: Int = 0,
countryName: Option[String] = None
) = {
val context = new Context()
val regions = countries.map(_.region).distinct.sorted.asJava
val selectedCountries = region match {
case None => countries
case Some("") => countries
case Some(selectedRegion) => countries.filter(_.region == selectedRegion)
}
val selectedCountries = getSelectedCountries(region, countryName)
val startIndex = page * pageSize
val countriesPage =
@@ -53,7 +53,9 @@ case class Routes(countries: List[Country])(implicit
context.setVariable("regionsSet", regions)
context.setVariable("countriesList", countriesPage.asJava)
context.setVariable("allCountriesList", countries.asJava)
// for anchor to request next pages from same countries
context.setVariable("selectedRegion", region.getOrElse(""))
context.setVariable("countryName", countryName.getOrElse(""))
val indexPage = engine.process("index", context)
Response(
@@ -62,28 +64,41 @@ case class Routes(countries: List[Country])(implicit
)
}
@cask.postForm("/countries-cards")
def postPageOfCountriesCards(
region: cask.FormValue,
countryName: cask.FormValue
) = {
println(s"in get for countries cards with $region and $countryName")
getPageOfCountriesCards(Some(region.value), 0, Some(countryName.value))
}
/** this method returns directly set of cards and new anchor for loading next
* page of cards
*
* intended to be called from "next-page-anchor" with htmx
*/
@cask.get("/countries-cards")
def getPageOfCountriesCards(region: Option[String] = None, page: Int = 0) = {
def getPageOfCountriesCards(
region: Option[String] = None,
page: Int = 0,
countryName: Option[String] = None
) = {
val context = new Context()
val selectedCountries = region match {
case None => countries
case Some("") => countries
case Some(selectedRegion) => countries.filter(_.region == selectedRegion)
}
val selectedCountries = getSelectedCountries(region, countryName)
val startIndex = page * pageSize
val countriesPage =
selectedCountries.slice(startIndex, startIndex + pageSize)
context.setVariable("countriesList", countriesPage.asJava)
// for anchor to request next pages from same countries
context.setVariable("selectedRegion", region.getOrElse(""))
context.setVariable("countryName", countryName.getOrElse(""))
// if current page is not full - there will be no next page
val nextPage = if (countriesPage.length == pageSize) page + 1 else -1
context.setVariable("countriesList", countriesPage.asJava)
context.setVariable("selectedRegion", region.getOrElse(""))
context.setVariable("nextPage", nextPage)
val cards = engine.process(
@@ -92,7 +107,7 @@ case class Routes(countries: List[Country])(implicit
context
)
// this is to store switch to another region in the history
val newUrl = s"/?region=${region.getOrElse("")}"
val newUrl = s"/?region=${region.getOrElse("")}&countryName=${countryName.getOrElse("")}"
// only save url when new region is requested, not on addtional card loads
val urlHeaderOpt = if (page == 0) Seq("HX-Push" -> newUrl) else Seq.empty
Response(
@@ -103,8 +118,38 @@ case class Routes(countries: List[Country])(implicit
)
}
private def getSelectedCountries(
region: Option[String] = None,
countryName: Option[String] = None
) = {
val countriesFromSelectedRegion = region match {
case None => countries
case Some("") => countries
case Some(selectedRegion) => countries.filter(_.region == selectedRegion)
}
// applying country name filtering, can be partial name
val selectedCountries = countryName match {
case None => countriesFromSelectedRegion
case Some("") => countriesFromSelectedRegion
case Some(partialSearchString) =>
val lowerSearch = partialSearchString.toLowerCase()
countriesFromSelectedRegion
.filter(country => {
val nameContainsPartialMatch =
country.name.common.toLowerCase().contains(lowerSearch) ||
country.name.nativeName.values.foldLeft(false) {
(acc, nativeName) =>
acc || nativeName.common.toLowerCase().contains(lowerSearch)
}
nameContainsPartialMatch
})
}
selectedCountries
}
@cask.get("/country")
def getCountryPage(countryName: String) = {
def getCountryPage(countryName: String, region: Option[String] = None) = {
val context = new Context()
countries.find(_.name.common == countryName) match {
case Some(selectedCountry) =>

View File

@@ -0,0 +1,18 @@
# Avoid accidental upload of the Sketch and Figma design files
#####################################################
## Please do not remove lines 5 and 6 - thanks! 🙂 ##
#####################################################
*.sketch
*.fig
# Avoid accidental XD upload if you convert the design file
###############################################
## Please do not remove line 12 - thanks! 🙂 ##
###############################################
*.xd
# Avoid your project being littered with annoying .DS_Store files!
.DS_Store
.prettierignore
public/out.css

View File

View File

@@ -0,0 +1,96 @@
* Frontend Mentor - Results summary component solution
:PROPERTIES:
:CUSTOM_ID: frontend-mentor---results-summary-component-solution
:END:
This is a solution to the
[[https://www.frontendmentor.io/challenges/results-summary-component-CE_K6s0maV][Results
summary component challenge on Frontend Mentor]]. Frontend Mentor
challenges help you improve your coding skills by building realistic
projects.
** Overview
:PROPERTIES:
:CUSTOM_ID: overview
:END:
*** The challenge
:PROPERTIES:
:CUSTOM_ID: the-challenge
:END:
Users should be able to:
- View the optimal layout for the interface depending on their device's
screen size
- See hover and focus states for all interactive elements on the page
- *Bonus*: Use the local JSON data to dynamically populate the content
*** Screenshot
:PROPERTIES:
:CUSTOM_ID: screenshot
:END:
[[screenshot-mobile.png]]
[[screenshot-desktop.png]]
*** Links
:PROPERTIES:
:CUSTOM_ID: links
:END:
- Solution URL: https://github.com/efim/Learning-HTMX/tree/master/17-results-summary-component-go
- Live Site URL: https://efim-frontentmentor-results-summary-go.onrender.com/
** My process
:PROPERTIES:
:CUSTOM_ID: my-process
:END:
*** Built with
:PROPERTIES:
:CUSTOM_ID: built-with
:END:
- Semantic HTML5 markup
- TailwindCSS
- Flexbox
- CSS Grid
- Mobile-first workflow
- Go Server Side Rendering
- Nix for building and docker image creation
*** What I learned
:PROPERTIES:
:CUSTOM_ID: what-i-learned
:END:
**** restarting server of file watch
:PROPERTIES:
:CUSTOM_ID: restarting-server-of-file-watch
:END:
go doesn't have built in restart on file watch, and there are separate programs that do that
#+begin_src bash
wgo -verbose -file .go -file .gohtml -file tailwind.config.js tailwindcss -i ./input.css -o public/out.css :: go run main.go -p 1234
#+end_src
and doing tailwind first, because server embeds the output css file, so
it's needed for the build step
**** setting up static routes with default go web server
**** generating template and returning it as response of go route
**** embedding files into the binary for portability
**** using css variables to set custom colors in TailwindCSS
**** getting cli arguments in the go program
*** Continued development
:PROPERTIES:
:CUSTOM_ID: continued-development
:END:
Further things to learn: getting data from forms, general go things - coroutines, utilizing sqlite, embedding PocketBase
*** Useful resources
:PROPERTIES:
:CUSTOM_ID: useful-resources
:END:
- https://pkg.go.dev/text/template#example-Template-Block
main go templating documentation
- https://pkg.go.dev/net/http
main default server documentation
- https://nixos.wiki/wiki/Go - packaging Go
- https://golangdocs.com/command-line-arguments-in-golang
command line arguments

View File

@@ -0,0 +1,22 @@
[
{
"category": "Reaction",
"score": 80,
"icon": "./assets/images/icon-reaction.svg"
},
{
"category": "Memory",
"score": 92,
"icon": "./assets/images/icon-memory.svg"
},
{
"category": "Verbal",
"score": 61,
"icon": "./assets/images/icon-verbal.svg"
},
{
"category": "Visual",
"score": 72,
"icon": "./assets/images/icon-visual.svg"
}
]

View File

@@ -0,0 +1,32 @@
{ lib, pkgs, ... }:
let
pname = "results-summary-component";
version = "0.0.1";
in rec {
package = pkgs.buildGoModule {
inherit pname version;
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
vendorHash = null; # set to "" when get dependencies in go.mod
# Adding the Tailwind build step to preBuild
preBuild = ''
${pkgs.nodePackages.tailwindcss}/bin/tailwindcss -i ./input.css -o public/out.css
'';
};
image = pkgs.dockerTools.buildLayeredImage {
name = pname;
tag = "latest";
created = "now";
config = {
Cmd = [
"${package}/bin/results-summary-component-go"
"-p"
"8080"
"-h"
"0.0.0.0"
];
ExposedPorts = { "8080/tcp" = { }; };
};
};
# nixos module
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,3 @@
module sunshine.industries/results-summary-component-go
go 1.20

View File

@@ -0,0 +1,34 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--summary-item-color-var: 0deg 100% 67%
}
}
html {
font-size: 18px;
}
@font-face {
font-family: 'HankenGrotesk';
src: url('/static/public/fonts/static/HankenGrotesk-Medium.ttf') format('truetype');
font-weight: 400; /* Regular */
font-style: normal;
}
@font-face {
font-family: 'HankenGrotesk';
src: url('/static/public/fonts/static/HankenGrotesk-Bold.ttf') format('truetype');
font-weight: 700; /* Bold */
font-style: normal;
}
@font-face {
font-family: 'HankenGrotesk';
src: url('/static/public/fonts/static/HankenGrotesk-ExtraBold.ttf') format('truetype');
font-weight: 800; /* ExtraBold */
font-style: normal;
}

View File

@@ -0,0 +1,79 @@
package main
import (
"embed"
"flag"
"fmt"
"html/template"
"io"
"log"
"net/http"
)
//go:embed public
var publicContent embed.FS
//go:embed templates
var templates embed.FS
type Category struct {
Name string
ColorHsl string
Score int
IconPath string
}
func iconPath(categoryName string) string {
return fmt.Sprintf("/static/public/images/icon-%s.svg", categoryName)
}
type ResultsSummaryData struct {
Categories []Category
TotalScore int
UpperPercent int
}
// starts webserver that serves html page for exercise
func main() {
// serves public static resources: bundled images, fonts, css
// visible on http://localhost:8080/static/public/some-text.txt
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(publicContent))))
http.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "This is temporary here, hello")
})
resultsData := ResultsSummaryData{
Categories: []Category{
{Name: "Reaction", ColorHsl: "0deg 100% 67%",
Score: 80, IconPath: iconPath("reaction")},
{Name: "Memory", ColorHsl: "39deg 100% 56%",
Score: 92, IconPath: iconPath("memory")},
{Name: "Verbal", ColorHsl: "166deg 100% 37%",
Score: 61, IconPath: iconPath("verbal")},
{Name: "Visual", ColorHsl: "234deg 85% 45%",
Score: 72, IconPath: iconPath("visual")},
},
TotalScore: 76,
UpperPercent: 65,
}
// main page with results summary
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
templateName := "templates/summary-component.gohtml"
tmpl := template.Must(template.ParseFS(templates, "templates/index.gohtml", templateName))
if err := tmpl.Execute(w, resultsData); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
var port int
var host string
flag.IntVar(&port, "p", 8080, "Specify port for server to start on")
flag.StringVar(&host, "h", "localhost", "Specify host for server to start on")
flag.Parse()
address := fmt.Sprintf("%s:%d", host, port)
log.Printf("starting server on %s", address)
log.Fatal(http.ListenAndServe(address, nil))
}

View File

@@ -0,0 +1,54 @@
Hanken Grotesk Variable Font
============================
This download contains Hanken Grotesk as both variable fonts and static fonts.
Hanken Grotesk is a variable font with this axis:
wght
This means all the styles are contained in these files:
HankenGrotesk-VariableFont_wght.ttf
If your app fully supports variable fonts, you can now pick intermediate styles
that arent available as static fonts. Not all apps support variable fonts, and
in those cases you can use the static font files for Hanken Grotesk:
static/HankenGrotesk-Medium.ttf
static/HankenGrotesk-Bold.ttf
static/HankenGrotesk-ExtraBold.ttf
Get started
-----------
1. Install the font files you want to use
2. Use your app's font picker to view the font family and all the
available styles
Learn more about variable fonts
-------------------------------
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
https://variablefonts.typenetwork.com
https://medium.com/variable-fonts
In desktop apps
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
Online
https://developers.google.com/fonts/docs/getting_started
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
Installing fonts
MacOS: https://support.apple.com/en-us/HT201749
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
Android Apps
https://developers.google.com/fonts/docs/android
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="#FFB21E" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25" d="M5.833 11.667a2.5 2.5 0 1 0 .834 4.858"/><path stroke="#FFB21E" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25" d="M3.553 13.004a3.333 3.333 0 0 1-.728-5.53m.025-.067a2.083 2.083 0 0 1 2.983-2.824m.199.054A2.083 2.083 0 1 1 10 3.75v12.917a1.667 1.667 0 0 1-3.333 0M10 5.833a2.5 2.5 0 0 0 2.5 2.5m1.667 3.334a2.5 2.5 0 1 1-.834 4.858"/><path stroke="#FFB21E" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25" d="M16.447 13.004a3.334 3.334 0 0 0 .728-5.53m-.025-.067a2.083 2.083 0 0 0-2.983-2.824M10 3.75a2.085 2.085 0 0 1 2.538-2.033 2.084 2.084 0 0 1 1.43 2.92m-.635 12.03a1.667 1.667 0 0 1-3.333 0"/></svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="#F55" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25" d="M10.833 8.333V2.5l-6.666 9.167h5V17.5l6.666-9.167h-5Z"/></svg>

After

Width:  |  Height:  |  Size: 247 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="#00BB8F" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25" d="M7.5 10h5M10 18.333A8.333 8.333 0 1 0 1.667 10c0 1.518.406 2.942 1.115 4.167l-.699 3.75 3.75-.699A8.295 8.295 0 0 0 10 18.333Z"/></svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="#1125D6" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25" d="M10 11.667a1.667 1.667 0 1 0 0-3.334 1.667 1.667 0 0 0 0 3.334Z"/><path stroke="#1125D6" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25" d="M17.5 10c-1.574 2.492-4.402 5-7.5 5s-5.926-2.508-7.5-5C4.416 7.632 6.66 5 10 5s5.584 2.632 7.5 5Z"/></svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,2 @@
hello!
yoyo

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1,49 @@
# Front-end Style Guide
## Layout
The designs were created to the following widths:
- Mobile: 375px
- Desktop: 1440px
## Colors
### Primary
- Light red: hsl(0, 100%, 67%)
- Orangey yellow: hsl(39, 100%, 56%)
- Green teal: hsl(166, 100%, 37%)
- Cobalt blue: hsl(234, 85%, 45%)
## Gradients
- Light slate blue (background): hsl(252, 100%, 67%)
- Light royal blue (background): hsl(241, 81%, 54%)
- Violet blue (circle): hsla(256, 72%, 46%, 1)
- Persian blue (circle): hsla(241, 72%, 46%, 0)
### Neutral
- White: hsl(0, 0%, 100%)
- Pale blue: hsl(221, 100%, 96%)
- Light lavender: hsl(241, 100%, 89%)
- Dark gray blue: hsl(224, 30%, 27%)
### Notes
Use transparency to get the colour variations necessary to match the design. Hint: look into using `hsla()`.
## Typography
### Body Copy
- Font size (paragraphs): 18px
### Font
- Family: [Hanken Grotesk](https://fonts.google.com/specimen/Hanken+Grotesk)
- Weights: 500, 700, 800

View File

@@ -0,0 +1,24 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.gohtml"],
theme: {
extend: {
colors: {
'light-slate-blue': 'hsl(252, 100%, 67%)',
'light-royal-blue': 'hsl(241, 81%, 54%)',
'violet-blue': 'hsla(256, 72%, 46%)',
'persian-blue': 'hsla(241, 72%, 46%)',
'pale-blue': 'hsl(221, 100%, 96%)',
'light-lavender': 'hsl(241, 100%, 89%)',
'dark-gray-blue': 'hsl(224, 30%, 27%)',
'summary-item-color': 'hsl(var(--summary-item-color-var) / <alpha-value>)',
},
'fontFamily': {
'sans': ['HankenGrotesk', 'sans-serif'],
'added': ['HankenGrotesk', 'sans-serif'],
},
},
},
plugins: [],
}

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- displays site properly based on user's device -->
<link
rel="stylesheet"
href="static/public/out.css"
type="text/css"
media="screen"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="static/public/images/favicon-32x32.png"
/>
<title>Frontend Mentor | Results summary component</title>
<!-- Feel free to remove these styles or customise in your own stylesheet 👍 -->
<style>
.attribution {
font-size: 11px;
text-align: center;
}
.attribution a {
color: hsl(228, 45%, 44%);
}
</style>
</head>
<body>
<main class="grid place-items-center w-screen h-screen">
{{ template "results-summary-component" . }}
</main>
<footer class="fixed inset-x-0 bottom-1 attribution">
Challenge by
<a href="https://www.frontendmentor.io?ref=challenge" target="_blank"
>Frontend Mentor</a
>. Coded by <a href="#">Your Name Here</a>.
</footer>
</body>
</html>

View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- displays site properly based on user's device -->
<link
rel="stylesheet"
href="static/public/out.css"
type="text/css"
media="screen"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="static/public/images/favicon-32x32.png"
/>
<title>THIS IS FILE FOR THE COMPONENT</title>
<!-- Feel free to remove these styles or customise in your own stylesheet 👍 -->
<style>
.attribution {
font-size: 11px;
text-align: center;
}
.attribution a {
color: hsl(228, 45%, 44%);
}
</style>
</head>
<body>
<main class="grid place-items-center w-screen h-screen">
{{/* only things inside of block component are used, yay */}}
{{ block "results-summary-component" . }}
<article
class="flex flex-col w-full h-full md:flex-row md:bg-white md:rounded-3xl md:shadow-2xl md:shadow-light-lavender/50 md:w-[715px] md:h-[500px]"
>
<section
id="results-pane"
class="flex flex-col gap-y-4 items-center px-10 pt-4 pb-8 text-white bg-gradient-to-t from-50% rounded-b-3xl md:justify-around md:w-1/2 md:rounded-3xl from-light-royal-blue to-light-slate-blue"
>
<h1 class="font-bold md:pt-2 md:text-xl text-pale-blue">
Your Result
</h1>
<div
class="flex flex-col justify-center items-center w-32 h-32 bg-gradient-to-t rounded-full md:w-44 md:h-44 from-violet-blue/60 to-persian-blue/75"
>
<span class="text-5xl font-extrabold md:text-6xl"
>{{ .TotalScore }}</span
>
<span class="text-pale-blue"> of 100 </span>
</div>
<div class="flex flex-col gap-y-3 items-center">
<h2 class="text-xl font-bold md:text-2xl">Great</h2>
<p class="text-sm text-center md:text-base text-pale-blue">
You scored higher than {{ .UpperPercent }}% of the people who have
taken these tests.
</p>
</div>
</section>
<section
id="summary-pane"
class="flex flex-col gap-y-4 p-7 md:justify-around md:px-8 md:w-1/2"
>
<h2 class="font-bold md:text-xl">Summary</h2>
<dl class="flex flex-col gap-y-4">
{{ range .Categories }}
<div
class="flex flex-row items-center px-5 h-12 text-sm rounded-lg bg-summary-item-color/5"
style="--summary-item-color-var: {{ .ColorHsl }}"
>
<img src="{{ .IconPath }}" alt="" class="pr-2" />
<dt class="flex-1 text-summary-item-color">{{ .Name }}</dt>
<dd class="flex flex-row">
<strong class="pr-2">{{ .Score }}</strong>
<span class="text-dark-gray-blue">/ 100</span>
</dd>
</div>
{{ end }}
</dl>
<a
href="/"
flex
flex-row
class="grid place-items-center w-full h-12 text-white rounded-full bg-dark-gray-blue"
>
Continue
</a>
</section>
</article>
{{ end }}
</main>
<footer class="fixed inset-x-0 bottom-1 attribution">
Challenge by
<a href="https://www.frontendmentor.io?ref=challenge" target="_blank"
>Frontend Mentor</a
>. Coded by <a href="#">Your Name Here</a>.
</footer>
</body>
</html>

1
18-expenses-chart/.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

5
18-expenses-chart/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/.direnv/
/.go/
*~
*_templ.go
static/output.css

View File

@@ -0,0 +1,28 @@
# List of all .templ files
TEMPL_FILES := $(shell find templates -type f -name '*.templ')
# Name of the generated file(s) from templ
GENERATED_FILES := $(TEMPL_FILES:.templ=_templ.go)
# Generate Go files from .templ files only if they have changed
$(GENERATED_FILES): $(TEMPL_FILES)
templ generate
TAILWIND_CONFIG := tailwind.config.js
INPUT_CSS := input.css
OUTPUT_CSS := static/output.css
# generate tailwindcss output
$(OUTPUT_CSS): $(TEMPL_FILES) $(TAILWIND_CONFIG) $(INPUT_CSS)
tailwindcss -i $(INPUT_CSS) -o $(OUTPUT_CSS)
# Run the server, with dependencies on templ and tailwind generation
run: $(GENERATED_FILES) $(OUTPUT_CSS)
go run .
# this uses wgo to re-execute 'make run'
# which will toggle templ generate, when needed
# and then do `go run .` to rebuild all else
.PHONY: run/live
run/live:
wgo -verbose -file=.go -file=.templ -file=$(INPUT_CSS) -file=$(TAILWIND_CONFIG) make run

View File

@@ -1,6 +1,6 @@
# Frontend Mentor - REST Countries API with color theme switcher solution
# Frontend Mentor - Expenses chart component solution
This is a solution to the [REST Countries API with color theme switcher challenge on Frontend Mentor](https://www.frontendmentor.io/challenges/rest-countries-api-with-color-theme-switcher-5cacc469fec04111f7b848ca). Frontend Mentor challenges help you improve your coding skills by building realistic projects.
This is a solution to the [Expenses chart component challenge on Frontend Mentor](https://www.frontendmentor.io/challenges/expenses-chart-component-e7yJBUdjwt). Frontend Mentor challenges help you improve your coding skills by building realistic projects.
## Table of contents
@@ -24,12 +24,11 @@ This is a solution to the [REST Countries API with color theme switcher challeng
Users should be able to:
- See all countries from the API on the homepage
- Search for a country using an `input` field
- Filter countries by region
- Click on a country to see more detailed information on a separate page
- Click through to the border countries on the detail page
- Toggle the color scheme between light and dark mode *(optional)*
- View the bar chart and hover over the individual bars to see the correct amounts for each day
- See the current days bar highlighted in a different colour to the other bars
- View the optimal layout for the content depending on their devices screen size
- See hover states for all interactive elements on the page
- **Bonus**: Use the JSON data file provided to dynamically size the bars on the chart
### Screenshot

View File

@@ -1,6 +1,6 @@
# Frontend Mentor - REST Countries API with color theme switcher
# Frontend Mentor - Expenses chart component
![Design preview for the REST Countries API with color theme switcher coding challenge](./design/desktop-preview.jpg)
![Design preview for the Expenses chart component coding challenge](./design/desktop-preview.jpg)
## Welcome! 👋
@@ -8,38 +8,35 @@ Thanks for checking out this front-end coding challenge.
[Frontend Mentor](https://www.frontendmentor.io) challenges help you improve your coding skills by building realistic projects.
**To do this challenge, you need a good understanding of HTML, CSS, and JavaScript.**
**To do this challenge, you need a decent understanding of HTML, CSS and JavaScript.**
## The challenge
Your challenge is to integrate with the [REST Countries API](https://restcountries.com) to pull country data and display it like in the designs.
Your challenge is to build out this bar chart component and get it looking as close to the design as possible.
You can use any JavaScript framework/library on the front-end such as [React](https://reactjs.org) or [Vue](https://vuejs.org). You also have complete control over which packages you use to do things like make HTTP requests or style your project.
You can use any tools you like to help you complete the challenge. So if you've got something you'd like to practice, feel free to give it a go.
We provide the data for the chart in a local `data.json` file. So you can use that to dynamically add the bars if you choose.
Your users should be able to:
- See all countries from the API on the homepage
- Search for a country using an `input` field
- Filter countries by region
- Click on a country to see more detailed information on a separate page
- Click through to the border countries on the detail page
- Toggle the color scheme between light and dark mode *(optional)*
- View the bar chart and hover over the individual bars to see the correct amounts for each day
- See the current day's bar highlighted in a different colour to the other bars
- View the optimal layout for the content depending on their device's screen size
- See hover states for all interactive elements on the page
- **Bonus**: See dynamically generated bars based on the data provided in the local JSON file
Want some support on the challenge? [Join our Slack community](https://www.frontendmentor.io/slack) and ask questions in the **#help** channel.
**⚠️ NOTE ⚠️: Sometimes the REST Countries API can go down. We've added a `data.json` file with all the country data if you prefer to use that instead. However, please be aware that the data in the JSON file might not be up-to-date.**
Want some support on the challenge? [Join our community](https://www.frontendmentor.io/community) and ask questions in the **#help** channel.
## Where to find everything
Your task is to build out the project to the designs inside the `/design` folder.
In this challenge, you will find mobile and desktop designs in light and dark mode color schemes for both pages.
Your task is to build out the project to the designs inside the `/design` folder. You will find both a mobile and a desktop version of the design.
The designs are in JPG static format. Using JPGs will mean that you'll need to use your best judgment for styles such as `font-size`, `padding` and `margin`.
If you would like the design files (we provide Sketch & Figma versions) to inspect the design in more detail, you can [subscribe as a PRO member](https://www.frontendmentor.io/pro).
There are no assets for this challenge, as the country flags will be pulled from the [REST Countries API](https://restcountries.com) and you can use an icon font library for the icons.
You will find all the required assets in the `/images` folder. The assets are already optimized.
There is also a `style-guide.md` file containing the information you'll need, such as color palette and fonts.
@@ -56,7 +53,7 @@ Feel free to use any workflow that you feel comfortable with. Below is a suggest
## Deploying your project
As mentioned above, there are many ways to host your project for free. Our recommend hosts are:
As mentioned above, there are many ways to host your project for free. Our recommended hosts are:
- [GitHub Pages](https://pages.github.com/)
- [Vercel](https://vercel.com/)
@@ -82,7 +79,7 @@ Remember, if you're looking for feedback on your solution, be sure to ask questi
There are multiple places you can share your solution:
1. Share your solution page in the **#finished-projects** channel of the [Slack community](https://www.frontendmentor.io/slack).
1. Share your solution page in the **#finished-projects** channel of the [community](https://www.frontendmentor.io/community).
2. Tweet [@frontendmentor](https://twitter.com/frontendmentor) and mention **@frontendmentor**, including the repo and live URLs in the tweet. We'd love to take a look at what you've built and help share it around.
3. Share your solution on other social channels like LinkedIn.
4. Blog about your experience building your project. Writing about your workflow, technical choices, and talking through your code is a brilliant way to reinforce what you've learned. Great platforms to write on are [dev.to](https://dev.to/), [Hashnode](https://hashnode.com/), and [CodeNewbie](https://community.codenewbie.org/).

View File

@@ -0,0 +1,30 @@
[
{
"day": "mon",
"amount": 17.45
},
{
"day": "tue",
"amount": 34.91
},
{
"day": "wed",
"amount": 52.36
},
{
"day": "thu",
"amount": 31.07
},
{
"day": "fri",
"amount": 23.39
},
{
"day": "sat",
"amount": 43.28
},
{
"day": "sun",
"amount": 25.48
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

211
18-expenses-chart/flake.lock generated Normal file
View File

@@ -0,0 +1,211 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1694102001,
"narHash": "sha256-vky6VPK1n1od6vXbqzOXnekrQpTL4hbPAwUhT5J9c9E=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "9e21c80adf67ebcb077d75bd5e7d724d21eeafd6",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gomod2nix": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1705314449,
"narHash": "sha256-yfQQ67dLejP0FLK76LKHbkzcQqNIrux6MFe32MMFGNQ=",
"owner": "nix-community",
"repo": "gomod2nix",
"rev": "30e3c3a9ec4ac8453282ca7f67fca9e1da12c3e6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "gomod2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1710097495,
"narHash": "sha256-B7Ea7q7hU7SE8wOPJ9oXEBjvB89yl2csaLjf5v/7jr8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d40e866b1f98698d454dad8f592fe7616ff705a4",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1701282334,
"narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"templ": "templ"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"templ": {
"inputs": {
"gitignore": "gitignore",
"gomod2nix": "gomod2nix",
"nixpkgs": "nixpkgs_2",
"xc": "xc"
},
"locked": {
"lastModified": 1709917943,
"narHash": "sha256-zDQxUFSmG/VX+xtK+nZ3ObRMVcMjjx+EUAxHLNcHHF8=",
"owner": "a-h",
"repo": "templ",
"rev": "df2a32403bb4a8e4745ac10ddc8b3e77386d8045",
"type": "github"
},
"original": {
"owner": "a-h",
"repo": "templ",
"type": "github"
}
},
"xc": {
"inputs": {
"flake-utils": "flake-utils_3",
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1703164129,
"narHash": "sha256-kCcCqqwvjN07H8FPG4tXsRVRcMqT8dUNt9pwW1kKAe8=",
"owner": "joerdav",
"repo": "xc",
"rev": "0655cccfcf036556aeaddfb8f45dc7e8dd1b3680",
"type": "github"
},
"original": {
"owner": "joerdav",
"repo": "xc",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -0,0 +1,80 @@
rec {
description = "expenses chart exercise";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
templ.url = "github:a-h/templ";
};
outputs = { self, nixpkgs, flake-utils, templ }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
templPkg = templ.packages.${system}.templ;
pname = "expenses-chart";
version = "0.0.1";
in rec {
thePackage = pkgs.buildGoModule {
inherit pname version;
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
vendorHash = "sha256-sKuP3TsRD3MXBGtdSRnX62eXBQE1ASWFQu0kLlXSlFA=";
preBuild = ''
# Adding the Tailwind build step to preBuild
${pkgs.nodePackages.tailwindcss}/bin/tailwindcss -i ./input.css -o static/output.css
# Adding generation of go code from templ files
${templPkg}/bin/templ generate
'';
};
packages = rec {
"${pname}" = thePackage;
"${pname}-image" = pkgs.dockerTools.buildLayeredImage {
name = pname;
tag = "latest";
created = "now";
config = {
Cmd = [ "${thePackage}/bin/templ-exercise" ];
ExposedPorts = { "3000/tcp" = { }; };
};
};
image-hello = pkgs.dockerTools.buildLayeredImage { # so, wow, this works
name = pname;
tag = "latest";
config.Cmd = [ "${pkgs.hello}/bin/hello" ];
};
image = pkgs.dockerTools.buildImage {
name = pname;
tag = "latest";
created = "now";
copyToRoot = pkgs.buildEnv {
name = "image-root";
paths = [ thePackage pkgs.dockerTools.binSh pkgs.coreutils ];
pathsToLink = [ "/bin" "/dist" "/public" ];
};
config = {
Cmd = [ "/bin/templ-exercise" ];
ExposedPorts = { "8080/tcp" = { }; };
};
};
};
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.go
pkgs.wgo
pkgs.semgrep
pkgs.gopls
pkgs.gnumake
templPkg
pkgs.nodePackages.tailwindcss
];
shellHook = ''
export GOPATH=$PWD/.go
export PATH=$GOPATH/bin:$PATH
'';
};
});
# see https://serokell.io/blog/practical-nix-flakes
}

View File

5
18-expenses-chart/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module sunshine.industries/temp-exercise
go 1.21.7
require github.com/a-h/templ v0.2.598

4
18-expenses-chart/go.sum Normal file
View File

@@ -0,0 +1,4 @@
github.com/a-h/templ v0.2.598 h1:6jMIHv6wQZvdPxTuv87erW4RqN/FPU0wk7ZHN5wVuuo=
github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

View File

@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
font-size: 18px;
}

33
18-expenses-chart/main.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"context"
"fmt"
"math/rand"
"net/http"
"sunshine.industries/temp-exercise/templates"
)
func randomDailyExpense() float32 {
return 10 + rand.Float32()*100
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
component := templates.IndexPage(templates.PageData{
Balance: 500 + rand.Float32()*1000,
Expenses: []float32{randomDailyExpense(), randomDailyExpense(), randomDailyExpense(), randomDailyExpense(), randomDailyExpense(), randomDailyExpense(), randomDailyExpense()},
TotalThisMonth: 50 + rand.Float32()*600,
PercentComparedToLastMonth: -5 + 10*rand.Float32(),
})
component.Render(context.Background(), w)
})
staticFs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", staticFs))
fmt.Println("starting to serve on :3000")
http.ListenAndServe(":3000", nil)
}

107
18-expenses-chart/notes.org Normal file
View File

@@ -0,0 +1,107 @@
* new exersice after long time
now will be learning https://github.com/a-h/templ
it's a templating solution for go, more similar to jsx
but it also needs a pre-processing step, so i'm adding it like tailwind
with Makefile for make run/live
and will need to add as a step in project build
* setting up
https://git.sunshine.industries/efim/golang-templ-and-tailwind
have separate repo, but i guess i'll remove .git from this dir,
to have single commits across Learning HTMX?
or maybe magit will just allow for simple integration?
just not pushing into origin, committing, and then committing from overall repository?
** not so simple, but still quite OK!
there's now 'subtree' which is separate concept from submodule,
and it is maybe simpler:
https://magit.vc/manual/magit/Subtree.html
i'm using the repo url
ssh://gitea@git.sunshine.industries:65433/efim/golang-templ-and-tailwind.git
and the command magit subtree add,
specify the directory, and it gets embedded it seems.
i have one level magit dispatch and my notes would be committed to the main repo,
i would potentially be able to pull in commits from the parent?
and maybe somehow contribute, but yeah, this is still confusing 🙂
how would i commit to subtree upstream if there are conflicting changes?
maybe i'll just get denied, as simple as that.
allright! let's add exercise specific data (again)
* so, starting dev
old process:
1. open second browser and open png with style
first mobile
2. enable view C-S-m in firefox
the responsive mode
i suppose next is adding colors to tailwind configuration?
from the styleguide file
also
3. check desktop style, whether any big rearrangements are required
so that i'd start mobile with way to rearrange things
do i have a separate note with all of these things?
i've also used some commands to vertically center my stuff
** DONE allright, tailwind config go
font, size, colors
** well, i don't really want to go on.
but yeah, i'll need hierarchy of the elements.
** ok. thoughs:
the componets would be
- my balance bubble
because it is spacially separate
- spending summary
- graph inside spending summary
or maybe don't even need that
the component should take up all space provided to it.
if parent would want to add space around - that's easy to do,
but if component insists on adding space around itself - harder to reuse?
because then if parent would want to have child take up all space - negative padding?
*** allright, @ to include sub templates
** well, last part? preparing the docker container for deployment on Render.com?
which port should be used?
also, this would be first time i'll add templ as a build step
** for some reason image doesn't work well
podman run -d -p 9090:8080 localhost/expenses-chart:latest /nix/store/l4r6glmzbvkhg97lp4dn7nm76w6hz41g-expenses-chart-0.0.1/bin/temp-exercise
this runs,
but podman run -it expenses-chart:latest
Error: crun: executable file `/nix/store/l4r6glmzbvkhg97lp4dn7nm76w6hz41g-expenses-chart-0.0.1/bin/templ-exercise` not found in $PATH: No such file or directory: OCI runtime attempted to invoke a command that was not found
oh, well
** you know what - i give up
for some reason when i open url to app running in container,
and it's on the 8080 port of the host machine - the static files are served
if i use any other port of local machine, to forward into container,
the static files are not served
maybe this is bug in go? i don't know
StripPrefix doesn't seem to do anything
and for some reason in this specific exercise CMD doesn't work in the container
so let's just give up!
i could try to write a docker file and check with that, but yeah

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg width="72" height="48" viewBox="0 0 72 48" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle fill="#382314" cx="48" cy="24" r="24"/><circle stroke="#FFF" stroke-width="2" cx="24" cy="24" r="23"/></g></svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1,37 @@
# Front-end Style Guide
## Layout
The designs were created to the following widths:
- Mobile: 375px
- Desktop: 1440px
> 💡 These are just the design sizes. Ensure content is responsive and meets WCAG requirements by testing the full range of screen sizes from 320px to large screens.
## Colors
### Primary
- Soft red: hsl(10, 79%, 65%)
- Cyan: hsl(186, 34%, 60%)
### Neutral
- Dark brown: hsl(25, 47%, 15%)
- Medium brown: hsl(28, 10%, 53%)
- Cream: hsl(27, 66%, 92%)
- Very pale orange: hsl(33, 100%, 98%)
## Typography
### Body Copy
- Font size: 18px
### Font
- Family: [DM Sans](https://fonts.google.com/specimen/DM+Sans)
- Weights: 400, 700
> 💎 [Upgrade to Pro](https://www.frontendmentor.io/pro?ref=style-guide) for design file access to see all design details and get hands-on experience using a professional workflow with tools like Figma.

View File

@@ -0,0 +1,29 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./templates/index.templ"],
theme: {
extend: {
fontFamily: {
'sans': ['DM Sans', 'sans-serif'],
},
fontWeight: {
'normal': 400,
'bold': 700,
}
},
colors: {
primary: {
'soft-red': 'hsl(10, 79%, 65%)',
cyan: 'hsl(186, 34%, 60%)',
},
neutral: {
'dark-brown': 'hsl(25, 47%, 15%)',
'medium-brown': 'hsl(28, 10%, 53%)',
cream: 'hsl(27, 66%, 92%)',
'very-pale-orange': 'hsl(33, 100%, 98%)',
},
}
},
plugins: [],
}

View File

@@ -0,0 +1,133 @@
package templates
import "fmt"
import "slices"
type PageData struct {
Balance float32
Expenses []float32
TotalThisMonth float32
PercentComparedToLastMonth float32
}
templ myBalanceComponent(balance float32) {
<div class="flex flex-row rounded-xl text-neutral-very-pale-orange text-xs md:text-base bg-primary-soft-red p-4 shadow md:rounded-[1rem] md:p-7">
<div class="grow ">
My balance
<p class="text-xl md:text-2xl font-bold">${ fmt.Sprintf("%.2f", balance) }</p>
</div>
<img
src="./static/images/logo.svg"
alt="App logo: a white circle overlapping from the left a filled in black circle"
/>
</div>
}
var days []string = []string{"mon", "tue", "wed", "thu", "fri", "sat", "sun"}
// returns templ SummaryComponent
// with all attributes computed from the expenses slice
// the percentages of the columns, current day number, etc
// NOTE: this seems to be the way to mix go calculations and templates
func prepareSummaryComponent(expenses []float32, totalThisMonth, percentComparedToLastMonth float32) templ.Component {
fmt.Println("hello, preparing expenses: ", expenses)
max := slices.Max(expenses)
percentages := make([]float32, 0, len(expenses))
for _, price := range expenses {
percentages = append(percentages, price/max)
}
currentDayNum := 2
return spendingSummaryComponent(expenses, percentages, currentDayNum, percentComparedToLastMonth, totalThisMonth)
}
css expenseBarVars(percentage float32) {
--height-percentage: { fmt.Sprintf("%.2f", percentage) };
height: calc(var(--height-percentage) * 8rem);
}
templ dayExpenseColumn(expense, percentage float32, day string, isCurrentDay bool) {
<div class="flex flex-col justify-end items-center h-32">
<div
class={ "rounded group relative w-full h-12",
expenseBarVars(percentage),
templ.KV("bg-primary-soft-red hover:bg-primary-soft-red/70", !isCurrentDay),
templ.KV("bg-primary-cyan hover:bg-primary-cyan/70", isCurrentDay),
"hover:cursor-pointer" }
>
<span class="absolute left-1/2 -translate-x-1/2 -translate-y-7 opacity-0 group-hover:opacity-100 bg-neutral-dark-brown text-neutral-cream rounded text-xs p-1">${ fmt.Sprintf("%.2f", expense ) }</span>
</div>
<p class="text-neutral-medium-brown text-xs py-2">{ day }</p>
</div>
}
// The 7 vertical bars of the per-day expenses
templ expensesChart(expenses, percentages []float32, currentDayNum int) {
<div class="grid grid-cols-7 place-content-between gap-x-3">
for i := 0; i < 7; i++ {
@dayExpenseColumn(expenses[i], percentages[i], days[i], currentDayNum == i)
}
</div>
}
// Big container with chard and total expenses of the week
templ spendingSummaryComponent(expenses, percentages []float32, currentDayNum int, totalThisMonth, percentComparedToLastMonth float32) {
<div class="bg-neutral-very-pale-orange rounded-xl shadow p-4 py-6 flex flex-col gap-y-4 md:p-7 md:px-10 md:rounded-[1rem] md:gap-y-5">
<p2 class="text-neutral-dark-brown text-xl font-bold pb-6 md:text-2xl md:pb-12 ">Spending - Last 7 days</p2>
@expensesChart(expenses, percentages, currentDayNum)
<hr class="bg-neutral-cream border-t-0 h-0.5"/>
<div class="grid grid-cols-2 text-sm text-neutral-medium-brown">
<p class="col-span-full">Total this month</p>
<div class="grow">
<p class="grid items-center text-neutral-dark-brown text-2xl font-bold h-full md:text-4xl md:py-2">$ { fmt.Sprintf("%.2f", totalThisMonth) }</p>
</div>
<div class="text-right grid content-center">
<div class="text-neutral-dark-brown font-bold ">
if (percentComparedToLastMonth > 0) {
<span class="inline">+</span>
}
{ fmt.Sprintf("%.2f", percentComparedToLastMonth) }%
</div>
from last month
</div>
</div>
</div>
}
templ WidgetsComponend(pageData PageData) {
<div class="flex flex-col gap-4">
@myBalanceComponent(pageData.Balance)
@prepareSummaryComponent(pageData.Expenses, pageData.PercentComparedToLastMonth, pageData.TotalThisMonth)
</div>
}
templ IndexPage(pageData PageData) {
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="/styles/templ.css"/>
<link href="/static/output.css" rel="stylesheet"/>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <!-- displays site properly based on user's device -->
<link rel="icon" type="image/png" sizes="32x32" href="./static/images/favicon-32x32.png"/>
<title>Frontend Mentor | Expenses chart component</title>
<!-- Feel free to remove these styles or customise in your own stylesheet 👍 -->
<style>
.attribution { font-size: 11px; text-align: center; }
.attribution a { color: hsl(228, 45%, 44%); }
</style>
</head>
<body class="bg-neutral-cream text-bold p-4 h-full grid place-items-center">
<main class="w-full md:w-1/4">
@WidgetsComponend(pageData)
</main>
<div class="attribution fixed bottom-0 inset-x-0">
Challenge by <a href="https://www.frontendmentor.io?ref=challenge" target="_blank">Frontend Mentor</a>.
Coded by <a href="#">Your Name Here</a>.
</div>
</body>
</html>
}

View File

@@ -36,16 +36,31 @@
inherit pkgs sbt-derivation;
lib = pkgs.lib;
};
results-component-go = import ./17-results-summary-component-go {
inherit pkgs;
lib = pkgs.lib;
};
in {
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.scala-cli
pkgs.sbt
pkgs.scalafmt
pkgs.jdk
pkgs.nodePackages.tailwindcss
pkgs.nodePackages.prettier
pkgs.jdk
pkgs.rustywind
pkgs.go
pkgs.wgo
pkgs.semgrep
pkgs.gopls
];
shellHook = ''
export GOPATH=$PWD/.go
export PATH=$GOPATH/bin:$PATH
'';
};
packages.price-grid-app = price-grid.package;
nixosModules.price-grid-app = price-grid.module;
@@ -64,6 +79,8 @@
nixosModules.multi-step-form = multi-step-form.module;
packages.countries-page = countries-page.package;
packages.countries-page-image = countries-page.image;
packages.results-component-go = results-component-go.package;
packages.results-component-go-image = results-component-go.image;
});
# see https://serokell.io/blog/practical-nix-flakes
}