Compare commits

...

197 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
efim
faedb21808 fix: flake naming for countries page 2023-09-27 06:14:20 +00:00
efim
9bdf180f97 feat: loading data from api on start 2023-09-27 05:41:38 +00:00
efim
f383085910 feat: changing to new rest countries api
as returned by https://restcountries.com/v3.1/all
2023-09-27 05:29:15 +00:00
efim
2070bbebb0 feat: adding nix docker image recipe 2023-09-27 04:49:48 +00:00
efim
9edf7f0196 fix: sorting borders put big buttons to end
and most of the buttons look prettier
2023-09-27 04:16:58 +00:00
efim
d7d5ac63ac feat: saving dark mode on page reload
main script is in index, country page template inserts it with
fragment by markup selector syntax:
https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#appendix-c-markup-selector-syntax

and still has some code to work in standalone
2023-09-27 04:15:11 +00:00
efim
841543e984 fix: input clipping on small desktop size 2023-09-27 03:46:43 +00:00
efim
b1c128738f feat: enabling jar build with nix 2023-09-26 16:03:51 +00:00
efim
51319b036b feat: dark theme, font icons
courtesy of https://github.com/tailwindlabs/heroicons
2023-09-26 16:01:37 +00:00
efim
0f30e1fc41 feat: desktop styling of country page 2023-09-25 06:46:39 +00:00
efim
11d5d6254a fix: swap beforeend to prevent nesting main
and url push from the server, because requesting cards now done on a
separate endpoint
2023-09-24 20:22:15 +00:00
efim
ae33d7e72a feat: index page for desktop 2023-09-24 19:58:05 +00:00
efim
aca8599d6e feat: infinite scroll 2023-09-24 14:36:26 +00:00
efim
43daef0455 fix: size errors in img and select 2023-09-24 14:03:50 +00:00
efim
ff296f84eb feat: adding icons
courtesy https://www.svgrepo.com/
2023-09-24 13:58:54 +00:00
efim
ae2b3d1327 feat: single country page, borders 2023-09-24 13:37:39 +00:00
efim
433ff827b7 feat: dummy country page and redirects
from search and cards.
card content wrapped in an anchor doesn't look good,
and search only working with htmx (without graceful degradatino) also
doesn't look too good

but with 'hx-boost="true"' ordinary anchor in card also preserves
header, that's nice
2023-09-23 19:17:09 +00:00
efim
b776000cf0 feat: selection filtering table update 2023-09-23 18:22:09 +00:00
efim
c260d348d7 feat: filtering countries on GET index 2023-09-23 16:28:14 +00:00
efim
153f5ef9ce feat: displaying country previews dynamically 2023-09-23 10:32:27 +00:00
efim
256df8d2aa feat: dynamic region input 2023-09-23 10:06:32 +00:00
efim
d300d5c16b feat: loading country data from resourse json 2023-09-23 09:31:04 +00:00
efim
73804a0ed4 feat: static mobile main page template 2023-09-23 08:31:40 +00:00
efim
4b2c6e4f62 feat: some header and control mockups 2023-09-23 06:54:48 +00:00
efim
7382680fc5 init: style guide for light theme 2023-09-23 05:55:07 +00:00
efim
cee8159c69 init: adding resource files 2023-09-23 04:17:48 +00:00
efim
6d5077159b init: added mainargs for port and host 2023-09-23 04:09:58 +00:00
efim
d74df846f2 init: added tailwindcss 2023-09-23 03:53:08 +00:00
efim
7a8dba19ee init: simple cask, initial template 2023-09-22 17:01:43 +00:00
efim
fe8b54346a docs: adding license to deployable projects 2023-07-22 06:04:42 +00:00
efim
fbeced6eb2 doc(15): link and more gratitute 2023-07-16 15:54:57 +00:00
efim
e17b45d2ec fix(15): sbt-derivation need new hach for new deps 2023-07-16 15:41:53 +00:00
efim
d7bcb418d4 docs(15): lessons learned 2023-07-16 15:34:33 +00:00
efim
77bcf87cd1 feat(15): phone validation htmx inline
making phone input as a fragment that accepts value and error
initial value from overall state, initial error empty string

some contitional classes and hidden error message
wowy,
endpoint that takes POST request, extracts value and resends the
rendered fragment. cool
2023-07-16 15:07:46 +00:00
efim
8c0318c4e2 fix(15): mobile 'months free' alignment 2023-07-16 07:03:56 +00:00
efim
080b47a9a6 feat(15): nix installation & image 2023-07-16 06:45:04 +00:00
efim
01ec722ea0 fix(15): selected plan was reset on mo\yr toggle
hx-put sends all form data, so full page state can be used to render the fragment
2023-07-16 05:10:37 +00:00
efim
e6f36dc0bd feat(15): styling sidebar font 2023-07-16 05:10:25 +00:00
efim
27e19d4494 feat(15): styling step 5 2023-07-16 04:55:10 +00:00
efim
5d0f58edc1 feat(15): step 4 desktop styling 2023-07-16 04:21:04 +00:00
efim
0e7d53b582 feat(15): stylings of steps 1-3
and mobile styling of step 4
2023-07-15 20:19:54 +00:00
efim
96edd50450 feat(15): adding only required validation 2023-07-15 17:31:25 +00:00
efim
dd590e1530 feat(15): jump from step4 to step2
here hidden 'form-confirmed' input will result in 'submitted' attribute
of Step4 answer to be set,
but this field is explicitly excluded on the hx-post from the Back
and Change links, this seems OK, i guess!
2023-07-15 17:15:21 +00:00
efim
3133a9aa8c feat(15): dynamicly redrawing plan type info yr/mo 2023-07-15 15:46:32 +00:00
efim
a52ab42c61 feat(15): steps summary as dynamic fragment
defined in step1, reused in other steps
2023-07-15 14:35:52 +00:00
efim
77e70c3536 fix(15): set skipped identical prices
so 150 + 10 + 20 + 20 was only 180
2023-07-15 13:49:51 +00:00
efim
4d9a7c3e12 feat(15): dynamic values in step 3 addon selection 2023-07-15 13:48:48 +00:00
efim
6d827365ac feat(15): dynamic rendering of plan types
currently only on page revisit.
for dynamic - need to use htmx swapping, with separate endpoing getting
updates value from whole element being a fragment
2023-07-15 13:21:26 +00:00
efim
f9c32fd7dc feat(15): putting state values into summary step 2023-07-15 12:55:22 +00:00
efim
998cc778e6 feat(15): adding dynamic form data source 2023-07-15 12:05:40 +00:00
efim
076dc76ca4 feat(15): submitting form data on step back 2023-07-15 12:05:04 +00:00
efim
5f260455cb feat(15): displaying existing answers in form step 2023-07-15 04:36:28 +00:00
efim
bf33858e41 feat(15): route to get previous form step 2023-07-15 03:50:51 +00:00
efim
6ed489835f feat(15): start form, save state, decode steps 2023-07-14 20:49:00 +00:00
efim
febc77032b feat(15): steps submit form and set next 2023-07-14 20:17:15 +00:00
efim
65bfe3077f feat(15): receiving form submission from step 1 2023-07-14 18:29:25 +00:00
efim
f8f1580fc7 feat(15): making fragments, loading based on step 2023-07-14 05:03:18 +00:00
efim
aca7ae8ecf feat(15): index placeholder for the form
to be htmx'ed into getting appropriate step for the session
2023-07-13 19:31:53 +00:00
efim
03aa9f8b94 feat(15): associating session with root page 2023-07-13 18:31:30 +00:00
efim
2d514e9258 feat(15): initial styling step 5 2023-07-13 06:15:53 +00:00
efim
3c51273cc3 feat(15): initial step 4 styling 2023-07-11 17:55:26 +00:00
efim
68bd81ec3c feat(15): initial styling of step 3 2023-07-11 17:24:32 +00:00
efim
ef7193530b feat(15): initial step 2 desktop styling 2023-07-11 16:36:28 +00:00
efim
35df226d25 feat(15): initial static step 2 styling
toggle with help from
https://tailwind-elements.com/docs/standard/forms/switch/
multi select via label

styling with "peer" modifier
2023-07-11 06:41:44 +00:00
efim
967864e72d feat(15): desktop styling step 1 2023-07-09 19:19:40 +00:00
efim
7b2060c5d0 feat(15): styling of mobile step 1 2023-07-09 18:22:14 +00:00
efim
8b62506f00 feat(15): initial styling footer-controls section 2023-07-09 18:04:43 +00:00
efim
d8e25ff95d feat(15): initial mobile step 1 styling 2023-07-09 15:28:47 +00:00
efim
808ba96049 init(15): registering fonts with tailwind 2023-07-09 15:25:27 +00:00
efim
05c4044ef3 init(15): adding exercise resources 2023-07-09 15:05:07 +00:00
efim
e9fcba7ad6 init(15): adding tailwind, yay 2023-07-08 18:41:46 +00:00
efim
a2418a810c init(15): server returns dummy index from template 2023-07-08 18:27:21 +00:00
efim
c27cb67c2a init(15): sbt project with mainargs 2023-07-08 17:06:32 +00:00
efim
084035fdcb docs(14): lessons learned in rock-paper-scissors 2023-07-07 05:01:41 +00:00
efim
e4a18da5c2 docs(13): lessons learned fragments in thymeleaf 2023-07-06 06:16:55 +00:00
efim
d73c39500b docs(12): lessons learned and docker deploy 2023-07-05 06:47:10 +00:00
efim
5dced77b4d docs(11): readme with lessons learned 2023-07-04 06:40:24 +00:00
efim
88e1d04e0d feat(11): docker image for price grid app 2023-07-04 05:19:57 +00:00
efim
572b63c82d feat(root): attempt to create combined project
right now struggling with making page relative links,
so that images would get pulled from "subproj/public" path
2023-07-03 07:34:41 +00:00
efim
f6d8a1de8d feat(14): nix derivation to build docker image 2023-07-03 06:23:46 +00:00
efim
80af2e0fd0 feat(14): nix installation module for rps 2023-07-02 12:51:45 +00:00
efim
9ddd9d3943 fix(14): halo should be around winning hand 2023-07-02 12:31:18 +00:00
efim
59cab44920 feat(14): header desktop styling 2023-07-02 12:07:13 +00:00
efim
a7dbfffa8e feat(14): styling of desktop rules modal 2023-07-02 11:50:47 +00:00
efim
b67b3cdc89 feat(14): adding initial rules modal overlay 2023-07-02 11:41:31 +00:00
efim
372fb5a6ad feat(14): end of game animation
couldn't make circles grow from the center, they still had shared top
point.
the templates are certainly not quite plesant to look at, with 2 states
being showed into same big template.

i suppose that animations for both hands in the showdow, and for single
hand in showdown would be better stored in separate files.

and maybe appended by the htmx replacements, but i'm feeling rushed
2023-07-02 10:11:10 +00:00
efim
0c8a9b91e0 fix(14): winning logic for rock 2023-07-02 07:22:37 +00:00
efim
18d91b742e fix(14): prettier animation 2023-07-02 07:22:28 +00:00
efim
69dc59a1ed feat(14): animated countdown to house choice 2023-07-02 07:17:28 +00:00
efim
73ccfba393 feat(14): update and persist scores 2023-07-02 06:58:33 +00:00
efim
4a5a13f6d4 fix(14): styling and text of message 2023-07-02 06:26:28 +00:00
efim
697985a480 feat(14): appearing message, new game request 2023-07-02 06:15:11 +00:00
efim
4e1f4f4a8e feat(14): setting up result message with data
i think i should be able to make it appear with delay just with css
(and that would also mean that my initial delayed call to get house
choice is also not necessary, but oh well, this is still nice practice)
2023-07-02 06:01:00 +00:00
efim
02a5f41800 refactor: make choices object, move out style
this should allow to write 'isBeating' function over choices.
and maybe move to enums
2023-07-02 05:26:23 +00:00
efim
54af09d356 feat(14): easy htmx automatic timed request 2023-07-01 20:07:53 +00:00
efim
de3d4781f0 feat(14): manual request of house choice
hodgepodge of 2 fragments which are put into house choice depending on
the state.

the alignment is done in a very not nice way.
2023-07-01 20:00:19 +00:00
efim
66013ae1c6 feat(14): template in showdown-table, show choice 2023-07-01 14:20:52 +00:00
efim
69a464a767 feat(14): controls generated from fragment
with their own ids and htmx requests to submit the vote
2023-07-01 13:40:48 +00:00
efim
560ce6896a feat(14): controls models for fragment rendering 2023-07-01 13:06:36 +00:00
efim
91c53429e0 feat(14): initial static section change
paper control switches "controls" to "showdown" - the fragment of
results page. yay
2023-07-01 13:05:55 +00:00
efim
726cadec19 feat(14): final static main stylig 2023-07-01 08:10:16 +00:00
efim
9fc2e959fa feat(14): initial static styling of showdown 2023-07-01 07:25:20 +00:00
efim
bed0387575 feat(14): styled static selection screen 2023-07-01 06:09:06 +00:00
efim
de431a4bc0 feat(14): styled 3 controls, with css vars
for size, colors, position, this should be what i need i hope
2023-07-01 05:38:59 +00:00
efim
364d8a737a feat: initial styling of control 2023-07-01 04:58:37 +00:00
efim
6aa5c25a5e feat(14): adding exercise resources and guide 2023-06-30 18:20:39 +00:00
efim
1c8bc38f29 feat(14): enabling tailwindcss
the command to generate
"tailwindcss -i ./src/input.css -o ./src/main/resources/public/output.css --watch"
and this should be same during dev and in install script,
but. we'll be committing the output.css, let's try that
2023-06-30 17:03:21 +00:00
efim
83c75ad3a9 feat(14): enabling public resource serving
this time with "staticResources" for files in 'main/resources' which
should be copied and put onto classpath by the scala compilation, yay
2023-06-30 16:40:40 +00:00
efim
ec5cac0680 feat(11): enabled cask server with simple template 2023-06-30 14:06:15 +00:00
efim
770b4f6e87 init(14): scala project def with main args 2023-06-30 13:47:20 +00:00
efim
b6ba1dfcd0 feat: installing htmx and toggling card size 2023-06-30 11:58:54 +00:00
efim
3e00aa5f3d feat(13): rest route get single testimonial card
not ideal, that i need separate file, even though i can use the fragment
from the main file.

and having th:eac in same place as th:fragment means rendering that
fragment and passing element over which i'm iterating doesn't help if
collection is empty
2023-06-30 03:44:41 +00:00
efim
c9195759a8 feat(13): dynamic grid desktop layout 2023-06-29 19:30:12 +00:00
efim
36b91cd5d1 feat(13): desktop grid styling 2023-06-29 19:09:30 +00:00
efim
4248d1b047 feat(13): encoding data on server 2023-06-29 18:18:33 +00:00
efim
657d7c7407 feat: repeating dynamic testimonial section 2023-06-29 16:59:55 +00:00
efim
951d364380 feat: styling statically all other testimonials 2023-06-29 16:13:05 +00:00
efim
71be19c677 feat: styled first testimony as static page
bg - relative from 'output.css'
image - relative from templates/index.html

ideally i'll figure out how to lay out things, so that relative paths
would be same as paths during deploy
2023-06-29 15:54:47 +00:00
efim
ec8c8bb678 feat: setting up mess of thymeleaf.
had problems with th:fragment, getting errors
Caused by: ognl.OgnlException: source is null for getProperty(null, "text")

now will try to style the page as if it's static page, and then add
thymeleaf things
2023-06-29 15:23:47 +00:00
efim
7fac41488c feat: project to play with simpler fragments
my problem was - thinking that th:fragment is not rendered
but it is, and i get null poniter error, because argument is not there

the object under name used in fragment should already be available, for
example take out of th:each
2023-06-29 15:20:59 +00:00
efim
2dff41f428 feat(13): style guide into config 2023-06-29 12:13:18 +00:00
efim
d181064165 feat(13): setting up tailwindcss 2023-06-29 11:35:03 +00:00
efim
df09abd9a4 feat(13) adding exercise resources 2023-06-29 11:32:14 +00:00
efim
d35bf9735c feat(13): starting simple web server 2023-06-28 15:35:34 +00:00
efim
28f2cf281a init(13): new sbt project, main args 2023-06-28 15:24:03 +00:00
efim
2f1804a9fd feat(12): nix installation of exercise 2023-06-28 12:51:03 +00:00
efim
abf0c7262d docs: saving some things about dev process 2023-06-28 12:28:59 +00:00
efim
520dd6de9c feat(12): moving order summary into template file 2023-06-28 12:16:52 +00:00
efim
1d7647bcdf feat(12): active states styled 2023-06-28 12:10:25 +00:00
efim
6cd6c8cc3e feat(12): desktop styling 2023-06-28 12:04:22 +00:00
efim
0816b79c26 feat(12): styling mobile 2023-06-28 11:53:11 +00:00
efim
578c5d7d05 feat(12): adding style guide to config 2023-06-28 08:55:01 +00:00
efim
b8c5a0f19b feat(12): tailwindcss init 2023-06-28 08:43:28 +00:00
efim
3e395b5034 feat(12): adding assets of the exercise 2023-06-28 08:35:31 +00:00
efim
77dca6b951 feat(12): plugin to restart dev server in sbt 2023-06-28 08:29:38 +00:00
efim
509c45b357 feat(12): adding thymeleaf lib and template resp 2023-06-28 07:18:00 +00:00
efim
d0f8cd771e init(12): new sbt project: static price component 2023-06-28 06:11:07 +00:00
efim
e23afdab6f feat(11): adding reverse proxy to module 2023-06-27 15:53:28 +00:00
efim
e618397eb8 fix(11): relative paths to resources 2023-06-27 14:37:43 +00:00
efim
f37fc0da11 fix(11): adding missing systemd options
Configure unit start rate limiting. Units which are started more than startLimitBurst times within an interval time interval are not permitted to start any more.
https://search.nixos.org/options?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=startLimit
2023-06-27 13:49:20 +00:00
efim
73bd2eba84 feat(11): initial module with a service 2023-06-27 13:19:23 +00:00
efim
e7594ef2eb feat: nix build enabled 2023-06-26 20:52:44 +00:00
efim
ef63cd474f feat: adding assebly
run with
 java -jar ./target/scala-3.3.0/priceGrid-assembly-0.0.1.jar
build with
assebly command in sbt
2023-06-26 18:50:36 +00:00
efim
44340882ab feat: moving to sbt for building
it will be able to build into nix with sbt-derivation
oh well
2023-06-26 18:42:42 +00:00
efim
dbe2cd47da feat: accepting port and host
using lihaoyi mainargs, yay
and running .main on custom cask.Main

had a problem with run arguments "port" and "host" having same name as
inner attributes of cask.Main and i guess infinite recursion when i
tried to reference outer args, the more you know
2023-06-26 16:43:05 +00:00
efim
a5402c1fe2 refactor(11): allow passing routes configuration 2023-06-26 15:37:30 +00:00
efim
e5210fb26f fix(11): style & sizes 2023-06-26 14:14:48 +00:00
efim
57e0347c53 feat(11): adding font from google fonts 2023-06-26 14:03:42 +00:00
efim
88fe55bf45 feat(11): styling for mobile and desktop 2023-06-26 13:58:59 +00:00
efim
5294516707 refactor(11): putting index.html into scalatags 2023-06-26 10:35:08 +00:00
efim
05cdea56de feat(11): adding static files and task style guide 2023-06-26 08:02:26 +00:00
efim
6fec3d54ee init(11): setting up tailwindcss 2023-06-26 06:55:28 +00:00
efim
4475943a98 init(11): simplest scala scaffolding 2023-06-26 06:55:23 +00:00
efim
fd75be6abe feat: enabling TailwindCSS styling
with manual running of cli:
tailwindcss -i ./src/input.css -o ./dist/output.css --watch
2023-06-26 06:01:03 +00:00
efim
98d6f77014 feat: returning some basic pages 2023-06-25 19:47:09 +00:00
317 changed files with 12330 additions and 18 deletions

12
.gitignore vendored
View File

@@ -2,3 +2,15 @@
.scala-build/ .scala-build/
.metals/ .metals/
.direnv .direnv
*/dist/
/11-single-price-grid-component/.bloop/
**/.bloop
**/project/project/
**/project/metals.sbt
**/project/.bloop
**/project/target/
**/target/
*/result
result

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

2
.scalafmt.conf Normal file
View File

@@ -0,0 +1,2 @@
version = "3.7.3"
runner.dialect = scala3

View File

@@ -0,0 +1,12 @@
.bsp/
.scala-build/
.metals/
.direnv
*/dist/
/11-single-price-grid-component/.bloop/
*/project/project/
*/project/metals.sbt
*/project/.bloop
*/project/target/
*/target/

View File

View File

@@ -0,0 +1,2 @@
version = "3.7.3"
runner.dialect = scala3

View File

@@ -0,0 +1,121 @@
* Frontend Mentor - Single price grid component solution
:PROPERTIES:
:CUSTOM_ID: frontend-mentor---single-price-grid-component-solution
:END:
This is a solution to the
[[https://www.frontendmentor.io/challenges/single-price-grid-component-5ce41129d0ff452fec5abbbc][Single
price grid 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 component depending on their device's
screen size
- See a hover state on desktop for the Sign Up call-to-action
*** Screenshot
:PROPERTIES:
:CUSTOM_ID: screenshot
:END:
[[./screenshot-desktop.png]]
[[./screenshot-mobile.png]]
*** Links
:PROPERTIES:
:CUSTOM_ID: links
:END:
- [[https://www.frontendmentor.io/solutions/tailwind-scala-ssr-scalatags-and-cask-deployed-with-docker-image-UPzyFXyf_L][Solution URL]]
- [[https://efim-frontentmentor-price-grid-component.onrender.com/][Live Site URL]]
** My process
:PROPERTIES:
:CUSTOM_ID: my-process
:END:
*** Built with
:PROPERTIES:
:CUSTOM_ID: built-with
:END:
- Semantic HTML5 markup
- TailwindCSS
- Scalatags
html generation on backend
- Cask
simple Scala web server, with annotations to mark routes and simple functions to process request
- Mobile-first workflow
- Nix
for building the application, nix module for easy install to servers with NixOS, and docker image for other deployment
*** What I learned
:PROPERTIES:
:CUSTOM_ID: what-i-learned
:END:
**** Setting up Cask server to serve static resources
Previously in Vite the /public directory was just automagically made available to the production build.
Now, with @cask.staticFiles("/public") the route /public would serve files from "public" directory
The path is relative, so directory from which the server is started is important.
But the thing works.
**** First time installing TailwindCSS with cli, without the frontend bundler integration
https://tailwindcss.com/docs/installation
#+begin_src bash
$ tailwindcss -i ./src/input.css -o ./dist/output.css --watch
#+end_src
Idea for this exercise was to generate `out.css` of the final TailwindCSS stylesheet into /dist directory, which would be in .gitignore
The problem I've encountered - the nix derivation doesn't like to have all of the files in the tmp build directory, so files are referenced and loaded by neat library function
#+begin_src nix
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
#+end_src
Which only puts unignored files into store.
So one time for the build step i'm using tailwind this way:
#+begin_src nix
buildPhase = ''
sbt assembly
tailwindcss -i ./src/input.css -o ./output.css
'';
#+end_src
and then copy resulting file where the server expects it to be.
**** Written a NixOS module with Systemd service and a Nginx reverse proxy
on my NixOS server i just need to reference the flake by the repository url, import the module, and then
#+begin_src nix
imports = [ inputs.htmx-examples.nixosModules.x86_64-linux.price-grid-app ];
services.priceGridService = {
enable = true;
host = "price-grid.frontendmentor.sunshine.industries";
port = 12345;
};
#+end_src
setting up config values, and =enable = true;= makes the server instantiate the systemd service, which will cover restarts and logs, and nginx reverse proxy.
**** Found out about =Workdir= setting of systemd servcie and =WorkingDir= of docker image
*** Continued development
:PROPERTIES:
:CUSTOM_ID: continued-development
:END:
This was first app (exercise #11) in the experiment with learning SSR in Scala, i've already completed exercise #14, and in the following steps I'm learning Thymeleaf templating engine, and HTMX - the library for extending html to make pages that allow easier partial page updates.
** Acknowledgments
:PROPERTIES:
:CUSTOM_ID: acknowledgments
:END:
My gratitude to render.com who are providing free tier for the service hosting from the docker image.
Which was necessary for me to submit the solution into frontendmentor.
And to DockerHub for hosting my docker images gratis as well.
And to Nix for their documentation!

View File

@@ -0,0 +1,23 @@
scalaVersion := "3.3.0"
fork := true
ThisBuild / version := "0.0.1"
ThisBuild / organization := "industries.sunshine"
lazy val singlePriceGridComponent = (project in file("."))
.settings(
name := "priceGrid",
assembly / mainClass := Some("pricegrid.App"),
libraryDependencies ++= Seq(
"com.lihaoyi" %% "cask" % "0.9.1",
"com.lihaoyi" %% "scalatags" % "0.12.0",
"com.lihaoyi" %% "mainargs" % "0.5.0"
),
libraryDependencies ++= Seq(
"com.lihaoyi" %% "cask" % "0.9.1" % Test,
"com.lihaoyi" %% "scalatags" % "0.12.0" % Test,
"com.lihaoyi" %% "mainargs" % "0.5.0" % Test
)
)

View File

@@ -0,0 +1,118 @@
{ pkgs, lib, sbt-derivation }:
let
pname = "price-grid-app";
package = sbt-derivation.lib.mkSbtDerivation {
inherit pkgs pname;
# ...and the rest of the arguments
version = "0.0.1";
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
nativeBuildInputs = [ pkgs.nodePackages.tailwindcss ];
buildPhase = ''
sbt assembly
tailwindcss -i ./src/input.css -o ./output.css
'';
# css path different from ordinary development,
# because .gitignore makes it unavailable during nix build
# anyway copied to correct place
installPhase = ''
mkdir -p $out/bin
cp target/scala-*/priceGrid-assembly-*.jar $out/bin/priceGridApp.jar
mkdir -p $out/bin/dist
cp ./output.css $out/bin/dist/output.css
cp -r public $out/bin/public
'';
depsSha256 = "sha256-aWLqnPXvchtNqpSfXo5sWyK2QFNn1GqToy58cWrML3U=";
};
module = { config, pkgs, ... }:
let cfg = config.services.priceGridService;
in {
options.services.priceGridService = {
enable = lib.mkEnableOption "My service";
port = lib.mkOption {
type = lib.types.int;
default = 8080;
description = "Port to listen on.";
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "Host to bind to.";
};
useNginx = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to use Nginx to proxy requests.";
};
};
config = lib.mkIf cfg.enable {
users.groups.price-grid-app-group = { };
users.users.price-grid-app-user = {
isSystemUser = true;
group = "price-grid-app-group";
};
systemd.services.price-grid-app =
let serverHost = if cfg.useNginx then "localhost" else cfg.host;
in {
description = "My Java Service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
startLimitIntervalSec = 30;
startLimitBurst = 10;
serviceConfig = {
ExecStart =
"${pkgs.jdk}/bin/java -jar ${package}/bin/priceGridApp.jar -p ${
toString cfg.port
} --host ${serverHost}";
WorkingDirectory = "${package}/bin";
Restart = "on-failure";
User = "price-grid-app-user";
Group = "price-grid-app-group";
};
};
services.nginx = lib.mkIf cfg.useNginx {
virtualHosts.${cfg.host} = {
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
};
};
};
};
image = pkgs.dockerTools.buildLayeredImage {
name = pname;
tag = "latest";
created = "now";
config = {
Cmd = [ "${pkgs.jdk}/bin/java" "-jar" "${package}/bin/priceGridApp.jar" "--host" "0.0.0.0" ];
ExposedPorts = {
"8080/tcp" = {};
};
WorkingDir = "${package}/bin";
};
};
# image = pkgs.dockerTools.buildImage {
# name = pname;
# tag = "latest";
# created = "now";
# copyToRoot = pkgs.buildEnv {
# name = "image-root";
# paths = [ package pkgs.dockerTools.binSh pkgs.coreutils ];
# pathsToLink = [ "/bin" "/dist" "/public" ];
# };
# config = {
# Cmd = [ "${pkgs.jdk}/bin/java" "-jar" "${package}/bin/priceGridApp.jar" "--host" "0.0.0.0" ];
# ExposedPorts = {
# "8080/tcp" = {};
# };
# WorkingDir = "${package}/bin";
# };
# };
in {
inherit package module image;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1 @@
sbt.version=1.9.0

View File

@@ -0,0 +1 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
font-size: 16px;
}
.attribution {
font-size: 11px;
text-align: center;
}
.attribution a {
color: hsl(228, 45% 44%);
}
/* div, p, h1, h2 { */
/* outline: 1px solid red; */
/* } */

View File

@@ -0,0 +1,42 @@
//> using dep com.lihaoyi::cask:0.9.1
//> using dep com.lihaoyi::scalatags:0.12.0
//> using dep com.lihaoyi::mainargs:0.5.0
package pricegrid
import scalatags.Text.all._
import scalatags.Text.tags2
import scalatags.Text.short
import mainargs.{main, arg, ParserForMethods}
object App {
@main
def run(@arg(name = "post", short = 'p', doc = "Port on which server will start serving.")
portArg: Int = 8080,
@arg(name = "host", doc = "Host on which server will start serving.")
hostArg: String = "localhost") = {
println(s"Will start server on ${hostArg}:${portArg}")
val server = new cask.Main {
override val allRoutes = Seq(AppRoutes())
override def port = portArg
override def host = hostArg
}
server.main(Array.empty)
}
def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
}
case class AppRoutes()(implicit cc: castor.Context,
log: cask.Logger) extends cask.Routes {
@cask.get("/")
def index() = Page.wholePageMarkup
@cask.staticFiles("/dist") // this is what path gets matched
def distFiles() = "dist"
@cask.staticFiles("/public") // this is what path gets matched
def publicFiles() = "public"
initialize()
}

View File

@@ -0,0 +1,135 @@
package pricegrid
import scalatags.Text.all._
import scalatags.Text.tags2
object Page {
/**
* Page content - grid layout of separate elements
* contains grid control styles
*/
lazy val bodyMarkup = body(
`class` := "bg-neutral-gray w-screen h-screen px-7 py-16 drop-shadow-2xl",
`class` := "md:grid md:place-content-center",
div(
`class` := "grid md:grid-cols-2 md:auto-rows-min",
`class` := "md:h-[475px] md:w-[640px]",
joinOurCommunity(
`class` := "md:col-span-2 rounded-t-md",
),
signup(
`class` := "md:rounded-bl-md",
),
whyUs(
`class` := "rounded-b-md md:rounded-bl-none",
)
),
footerMarkup
)
lazy val joinOurCommunity = div(
`class` := "bg-white flex flex-col gap-y-4 p-6 py-7 ",
`class` := "md:px-10 md:gap-y-0",
h1(
`class` := "text-xl text-primary-cyan font-bold",
`class` := "md:text-2xl md:my-3",
"Join our community"
),
h2(
`class` := "text-sm text-primary-yellow font-bold",
`class` := "md:text-lg md:my-2",
"30-day, hassle-free money back guarantee"
),
p(
`class` := "text-xs text-grayish-blue leading-loose ",
`class` := "md:text-base md:leading-relaxed md:mb-2",
"""
Gain access to our full library of tutorials along with expert code reviews.
Perfect for any developers who are serious about honing their skills.
"""
)
)
lazy val signup = div(
`class` := "bg-bg-subscription text-white p-6 ",
`class` := "md:p-10",
h2(
`class` := "text-md font-bold mb-4 md:text-lg",
"Monthly Subscription"
),
div(
`class` := "flex flex-row items-center mb-2",
p(`class` := "text-3xl font-bold md:text-3xl", "$29"),
p(`class` := "font-extralight text-sm ml-3 text-neutral-gray", "per month")
),
p(`class` := "text-sm", "Full access for less than $1 a day"),
button(
`class` := "w-full bg-primary-yellow rounded-lg h-12 mt-7 drop-shadow-xl font-bold",
`class` := "md:text-md",
"Sign Up"
)
)
lazy val whyUs = div(
`class` := "bg-bg-why-us text-white p-6 md:text-lg",
`class` := "md:p-10",
h2(
`class` := "font-bold mb-3",
"Why Us"),
ul(
List(
" Tutorials by industry experts ",
" Peer & expert code review ",
" Coding exercises ",
" Access to our GitHub repos ",
" Community forum ",
" Flashcard decks ",
" New videos every week "
).map(linkText =>
li(
`class` := "text-xs text-neutral-gray pt-1 md:font-light md:text-sm md:leading-tight",
linkText)
)
)
)
lazy val wholePageMarkup = doctype("html")(
html(
lang := "en",
head(
meta(charset := "UTF-8"),
meta(
name := "viewport",
content := "width=device-width, initial-scale=1.0"
),
tags2.title("Frontend Mentor | Single Price Grid Component"),
link(
href := "https://fonts.googleapis.com/css2?family=Karla:wght@400;700&display=swap",
rel := "stylesheet",
),
link(
rel := "icon",
`type` := "image/png",
href := "/public/images/favicon-32x32.png"
),
link(rel := "stylesheet", href := "/dist/output.css")
),
bodyMarkup
)
)
lazy val footerMarkup = footer(
p(
cls := "attribution fixed bottom-0 inset-x-0",
"Challenge by ",
a(
href := "https://www.frontendmentor.io?ref=challenge",
target := "_blank",
"Frontend Mentor. "
),
"Source code at",
a(href := "https://github.com/efim/Learning-HTMX", "Your Name Here")
)
)
}

View File

@@ -0,0 +1,31 @@
# Front-end Style Guide
## Layout
The designs were created to the following widths:
- Mobile: 375px
- Desktop: 1440px
## Colors
### Primary
- Cyan: hsl(179, 62%, 43%)
- Bright Yellow: hsl(71, 73%, 54%)
### Neutral
- Light Gray: hsl(204, 43%, 93%)
- Grayish Blue: hsl(218, 22%, 67%)
## Typography
### Body Copy
- Font size: 16px
### Font
- Family: [Karla](https://fonts.google.com/specimen/Karla)
- Weights: 400, 700

View File

@@ -0,0 +1,25 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.scala"],
theme: {
extend: {
fontFamily: {
'sans': ['Karla', 'sans-serif']
},
fontWeight: {
'normal': 400,
'bold': 700,
},
colors: {
'primary-cyan': 'hsl(179, 62%, 43%)',
'primary-yellow': 'hsl(71, 73%, 54%)',
'neutral-gray': 'hsl(204, 43%, 93%)',
'grayish-blue': 'hsl(218, 22%, 67%)',
'bg-subscription': 'hsl(179, 61%, 44%)',
'bg-why-us': 'hsl(179, 47%, 52%)',
}
},
},
plugins: [],
}

View File

@@ -0,0 +1,16 @@
.bsp/
.scala-build/
.metals/
.direnv
*/dist/
/11-single-price-grid-component/.bloop/
**/.bloop
**/project/project/
**/project/metals.sbt
**/project/.bloop
**/project/target/
**/target/
*/result
result

View File

@@ -0,0 +1,2 @@
version = "3.7.3"
runner.dialect = scala3

View File

@@ -0,0 +1,105 @@
* Frontend Mentor - Order summary card solution
:PROPERTIES:
:CUSTOM_ID: frontend-mentor---order-summary-card-solution
:END:
This is a solution to the
[[https://www.frontendmentor.io/challenges/order-summary-component-QlPmajDUj][Order
summary card 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 hover states for interactive elements
*** Screenshot
:PROPERTIES:
:CUSTOM_ID: screenshot
:END:
[[screenshot-desktop.png]]
[[screenshot-mobile.png]]
*** Links
:PROPERTIES:
:CUSTOM_ID: links
:END:
- [[https://www.frontendmentor.io/solutions/responsive-by-tailwindcss-ssr-on-scala-with-cask-thymeleaf-template-bMMgFdajHT][Solution URL]]
- [[https://efim-frontentmentor-order-summary.onrender.com/][Live Site URL]]
Free instance on render.com is turned off after 15 minutes of inactivity, please give the server a moment to start up if you're visiting it at the moment of being turned off
** My process
:PROPERTIES:
:CUSTOM_ID: my-process
:END:
*** Built with
:PROPERTIES:
:CUSTOM_ID: built-with
:END:
- Semantic HTML5 markup
- Tailwincss
- Mobile-first workflow
- SSR in Scala & Cask webserver
- Thymeleaf templates
**** running during development
installing "sbt-revolver":
#+begin_src scala
addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")
#+end_src
then `~reStart -p 49012`
otherwise =~= in front of sbt command reruns it only if it finished execution.
*** What I learned
:PROPERTIES:
:CUSTOM_ID: what-i-learned
:END:
**** placing Thymeleaf templates
if they are in src/main/resources - they should be available on class path.
and search should be relative to the resources, i.e putting templates into dir "templates"
then
#+begin_src scala
val templateResolver = new ClassLoaderTemplateResolver()
templateResolver.setPrefix("templates/");
templateResolver.setSuffix(".html")
val result = templateEngine.process("index", context)
#+end_src
will look for file in "src/main/resources/templates/index.html" to treat as a template
**** to reload web server during development - sbt plugin "sbt-revolver"
**** with these html templates I can start learning and using Emmet mode in Emacs
https://github.com/smihica/emmet-mode
**** Importing Thymeleaf template
There are other methods to include templates like th:replace and th:include, which have slightly different behaviours. th:insert keeps the host tag (the div in your case), th:replace replaces the whole host tag with the fragment, and th:include replaces the inner content of the host tag with the fragment.
So i can actually have the "Order Summary" as reusable part.
**** getting responsive background image with Tailwincss
#+begin_src html
<div
class="bg-[url('../public/images/pattern-background-mobile.svg')] fixed h-screen w-screen bg-no-repeat bg-contain md:bg-[url('../public/images/pattern-background-desktop.svg')]"
></div>
#+end_src
*** Continued development
:PROPERTIES:
:CUSTOM_ID: continued-development
:END:
This was first exercise I've done with Thymeleaf templates.
Next is using fragments with some dynamic content and styling.
Then attempting to include htmx fueled reactivity.
** Acknowledgments
:PROPERTIES:
:CUSTOM_ID: acknowledgments
:END:
Lots of gratitude to Thymeleaf templates, they seem to be very advandced and thoughtfully designed

View File

@@ -0,0 +1,21 @@
ThisBuild / scalaVersion := "3.2.2"
fork := true
ThisBuild / version := "0.0.1"
ThisBuild / organization := "industries.sunshine"
val toolkitV = "0.1.7"
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV
lazy val orderSummaryComponent = (project in file("."))
.settings(
name := "order-summary-component",
libraryDependencies += toolkit,
libraryDependencies += (toolkitTest % Test),
libraryDependencies ++= Seq(
"com.lihaoyi" %% "cask" % "0.9.1",
"com.lihaoyi" %% "mainargs" % "0.5.0",
"org.thymeleaf" % "thymeleaf" % "3.1.1.RELEASE"
)
)

View File

@@ -0,0 +1,101 @@
{ pkgs, lib, sbt-derivation }:
let
pname = "order-summary-component-app";
package = sbt-derivation.lib.mkSbtDerivation {
inherit pkgs pname;
# ...and the rest of the arguments
version = "0.0.1";
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
nativeBuildInputs = [ pkgs.nodePackages.tailwindcss ];
buildPhase = ''
sbt assembly
tailwindcss -i ./src/input.css -o ./output.css
'';
# css path different from ordinary development,
# because .gitignore makes it unavailable during nix build
# anyway copied to correct place
installPhase = ''
mkdir -p $out/bin
cp target/scala-*/order-summary-component-assembly-*.jar $out/bin/${pname}.jar
mkdir -p $out/bin/dist
cp ./output.css $out/bin/dist/output.css
cp -r public $out/bin/public
'';
depsSha256 = "sha256-ADQB4qTl69ERlLAURrtR3fWa7PUdYjFLk5QdU5QgxRQ=";
};
in {
inherit package;
module = { config, pkgs, ... }:
let cfg = config.services.orderSummaryComponent;
in {
options.services.orderSummaryComponent = {
enable = lib.mkEnableOption "My service";
port = lib.mkOption {
type = lib.types.int;
default = 8080;
description = "Port to listen on.";
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "Host to bind to.";
};
useNginx = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to use Nginx to proxy requests.";
};
};
config = lib.mkIf cfg.enable {
users.groups.order-summary-component-group = { };
users.users.order-summary-component-user = {
isSystemUser = true;
group = "order-summary-component-group";
};
systemd.services.orderSummaryComponent =
let serverHost = if cfg.useNginx then "localhost" else cfg.host;
in {
description = "Exercise app Order Summary Component";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
startLimitIntervalSec = 30;
startLimitBurst = 10;
serviceConfig = {
ExecStart =
"${pkgs.jdk}/bin/java -jar ${package}/bin/${pname}.jar -p ${
toString cfg.port
} --host ${serverHost}";
WorkingDirectory = "${package}/bin";
Restart = "on-failure";
User = "order-summary-component-user";
Group = "order-summary-component-group";
};
};
services.nginx = lib.mkIf cfg.useNginx {
virtualHosts.${cfg.host} = {
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
};
};
};
};
image = pkgs.dockerTools.buildLayeredImage {
name = pname;
tag = "latest";
created = "now";
config = {
Cmd = [ "${pkgs.jdk}/bin/java" "-jar" "${package}/bin/${pname}.jar" "--host" "0.0.0.0" ];
ExposedPorts = {
"8080/tcp" = {};
};
WorkingDir = "${package}/bin";
};
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1 @@
sbt.version=1.9.0

View File

@@ -0,0 +1,3 @@
addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")

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="48" height="48"><g fill="none" fill-rule="evenodd"><circle cx="24" cy="24" r="24" fill="#DFE6FB"/><path fill="#717FA6" fill-rule="nonzero" d="M32.574 15.198a.81.81 0 00-.646-.19L20.581 16.63a.81.81 0 00-.696.803V26.934a3.232 3.232 0 00-1.632-.44A3.257 3.257 0 0015 29.747 3.257 3.257 0 0018.253 33a3.257 3.257 0 003.253-3.253v-8.37l9.726-1.39v5.327a3.232 3.232 0 00-1.631-.441 3.257 3.257 0 00-3.254 3.253 3.257 3.257 0 003.254 3.253 3.257 3.257 0 003.253-3.253V15.81a.81.81 0 00-.28-.613z"/></g></svg>

After

Width:  |  Height:  |  Size: 549 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="437"><path fill="#D6E1FF" fill-rule="evenodd" d="M0 349.974c218.558 116.035 460.05 116.035 724.475 0s502.933-116.035 715.525 0V0H0v349.974z"/></svg>

After

Width:  |  Height:  |  Size: 209 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="375" height="194"><path fill="#D6E1FF" fill-rule="evenodd" d="M-131.808 155.366c97.026 51.512 204.233 51.512 321.62 0 117.388-51.512 223.27-51.512 317.648 0V0h-639.268v155.366z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -0,0 +1,11 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
font-size: 16px;
}
/* div, p, button { */
/* outline: 1px solid red; */
/* } */

View File

@@ -0,0 +1 @@
<p>Hello, <span th:text="${name}">Name</span>!</p>

View File

@@ -0,0 +1,49 @@
<!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="icon"
type="image/png"
sizes="32x32"
href="./public/images/favicon-32x32.png"
/>
<link href="/dist/output.css" rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=Red+Hat+Display:wght@500;700;900&display=swap"
rel="stylesheet"
/>
<title>Frontend Mentor | Order summary card</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>
<!-- too bad that images in TailwindCSS classes are referenced from output.css -->
<div
class="bg-[url('../public/images/pattern-background-mobile.svg')] fixed h-screen w-screen bg-no-repeat bg-contain md:bg-[url('../public/images/pattern-background-desktop.svg')]"
></div>
<!-- main container -->
<div class="bg-pale-blue w-screen h-screen grid place-content-center">
<div th:replace="order-summary" />
</div>
<div class="attribution fixed bottom-0 inset-x-0">
Challenge by
<a href="https://www.frontendmentor.io?ref=challenge" target="_blank"
>Frontend Mentor</a
>. Source code at <a href="https://github.com/efim/Learning-HTMX">Your Name Here</a>.
</div>
</body>
</html>

View File

@@ -0,0 +1,37 @@
<div
class="bg-white rounded-xl drop-shadow-2xl flex flex-col items-center w-[320px] h-[565px] md:w-[450px] md:h-[700px]"
>
<img src="public/images/illustration-hero.svg" class="rounded-t-xl w-full" />
<h1 class="text-xl font-extrabold my-7 md:text-3xl md:mt-10">
Order Summary
</h1>
<p class="text-center text-desaturated-blue px-8 mb-5 md:px-12">
You can now listen to millions of songs, audiobooks, and podcasts on any
device anywhere you like!
</p>
<div
class="bg-very-pale-blue rounded-xl p-4 w-[calc(100%-3rem)] flex flex-row md:w-[calc(100%-6rem)] md:h-24 md:items-center md:p-6"
>
<img src="public/images/icon-music.svg" class="md:w-12 md:h-12" />
<div class="flex flex-col grow pl-3 md:pl-6 justify-center">
<p class="font-bold text-sm text-dark-blue md:text-base">Annual Plan</p>
<p class="text-desaturated-blue text-sm md:text-base">$59.99/year</p>
</div>
<button
class="underline text-xs text-bright-blue font-bold hover:no-underline hover:text-warm-blue"
>
Change
</button>
</div>
<button
class="bg-bright-blue rounded-xl drop-shadow-2xl text-white font-bold py-3 my-6 w-[calc(100%-3rem)] md:w-[calc(100%-6rem)] md:my-8 hover:bg-warm-blue"
>
Proceed to Payment
</button>
<button class="text-desaturated-blue font-bold hover:text-dark-blue">
Cancel Order
</button>
</div>

View File

@@ -0,0 +1,67 @@
//> using dep com.lihaoyi::cask:0.9.1
//> using dep com.lihaoyi::mainargs:0.5.0
//> using dep org.thymeleaf:thymeleaf:3.1.1.RELEASE
package example
import mainargs.{main, arg, ParserForMethods}
object Main {
@main def run(
@arg(
name = "post",
short = 'p',
doc = "Port on which server will start serving."
)
portArg: Int = 8080,
@arg(name = "host", doc = "Host on which server will start serving.")
hostArg: String = "localhost"
): Unit = {
println(s"Will start server on ${hostArg}:${portArg}")
val server = new cask.Main {
override val allRoutes = Seq(AppRoutes())
override def port = portArg
override def host = hostArg
}
server.main(Array.empty)
}
def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
case class AppRoutes()(implicit cc: castor.Context, log: cask.Logger)
extends cask.Routes {
@cask.get("/")
def index() = {
import org.thymeleaf.context.Context
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
import org.thymeleaf.TemplateEngine
val templateResolver = new ClassLoaderTemplateResolver()
templateResolver.setPrefix("templates/");
templateResolver.setSuffix(".html")
templateResolver.setTemplateMode("HTML5")
val templateEngine = new TemplateEngine()
templateEngine.setTemplateResolver(templateResolver)
val a = 11
val context = new Context()
context.setVariable("name", s"Johny $a")
val result = templateEngine.process("index", context)
cask.Response(
result,
headers = Seq("Content-Type" -> "text/html;charset=UTF-8")
)
}
@cask.staticFiles("/dist") // this is what path gets matched
def distFiles() = "dist"
@cask.staticFiles("/public") // this is what path gets matched
def publicFiles() = "public"
initialize()
}
}

View File

@@ -0,0 +1,8 @@
package example
class ExampleSuite extends munit.FunSuite:
test("addition") {
assert(1 + 1 == 2)
}
end ExampleSuite

View File

@@ -0,0 +1,32 @@
# Front-end Style Guide
## Layout
The designs were created to the following widths:
- Mobile: 375px
- Desktop: 1440px
## Colors
### Primary
- Pale blue: hsl(225, 100%, 94%)
- Bright blue: hsl(245, 75%, 52%)
### Neutral
- Very pale blue: hsl(225, 100%, 98%)
- Desaturated blue: hsl(224, 23%, 55%)
- Dark blue: hsl(223, 47%, 23%)
## Typography
### Body Copy
- Font size (paragraph): 16px
### Font
- Family: [Red Hat Display](https://fonts.google.com/specimen/Red+Hat+Display)
- Weights: 500, 700, 900

View File

@@ -0,0 +1,27 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.html"],
theme: {
extend: {
colors: {
'pale-blue': 'hsl(225, 100%, 94%)',
'bright-blue': 'hsl(245, 75%, 52%)',
'very-pale-blue': 'hsl(225, 100%, 98%)',
'desaturated-blue': 'hsl(224, 23%, 55%)',
'dark-blue': 'hsl(223, 47%, 23%)',
'warm-blue': 'hsl(245, 83%, 68%)',
},
fontFamily: {
'sans': ['Red Hat Display', 'sans-serif'], // This will set Roboto as the default sans font
},
fontWeight: {
'normal': 500,
'bold': 700,
'extrabold': 900,
}
},
},
plugins: [],
}

16
13-testimonials-grid-section/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
.bsp/
.scala-build/
.metals/
.direnv
*/dist/
/11-single-price-grid-component/.bloop/
**/.bloop
**/project/project/
**/project/metals.sbt
**/project/.bloop
**/project/target/
**/target/
*/result
result

View File

View File

@@ -0,0 +1,2 @@
version = "3.7.3"
runner.dialect = scala3

View File

@@ -0,0 +1,101 @@
* Frontend Mentor - Testimonials grid section solution
:PROPERTIES:
:CUSTOM_ID: frontend-mentor---testimonials-grid-section-solution
:END:
This is a solution to the
[[https://www.frontendmentor.io/challenges/testimonials-grid-section-Nnw6J7Un7][Testimonials
grid section 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 site depending on their device's
screen size
*** Screenshot
:PROPERTIES:
:CUSTOM_ID: screenshot
:END:
[[screenshot-desktop.png]]
[[screenshot-mobile.png]]
*** Links
:PROPERTIES:
:CUSTOM_ID: links
:END:
- [[https://efim-frontentmentor-testimoinals-grid.onrender.com/][Solution URL]]
- Live Site URL
** My process
:PROPERTIES:
:CUSTOM_ID: my-process
:END:
*** Built with
:PROPERTIES:
:CUSTOM_ID: built-with
:END:
- Semantic HTML5 markup
- TailwindCSS
- CSS Grid
- SSR on Scala with Cask
- Thymeleaf templates
*** What I learned
:PROPERTIES:
:CUSTOM_ID: what-i-learned
:END:
**** defining template fragments in Thymeleaf that take parameters
Bigger thing i didn't understand is that element marked by "th:fragment" are also rendered.
I had a problem with 'th:each' being defined on the same element as 'th:fragment'
even though due to th:each single 'testimonial' object is available for inserting. if i try to use that singular 'testimonial' as fragment argument, the also existing 'th:each' over "testimonials" runs and sinse there's no passed list of objects - nothing gets rendered.
So 'th:each' should be around the single fragment in the future.
Currently i've hacked this by passing List(testimonial) for re-rendering a single item.
**** A way to style fragments from the code
I've initially implemented the static page, with manually settin different testimonial colors and sizes, but then implemented a template fragment, which uses "th:classappend" to add tailwind color and size classes from the context objects.
This way unfortunately the tag marked with "th:fragement" (and it is getting rendered when template file is opened as a static file) doesn't have stylings.
And I'd really like a way that allows for having a fully displayed static template, which doesn't interfere with rendering.
Also - had to remember to not have space in
content: ["./src/**/*.{html,scala}"],
so that TailwindCSS would also monitor classes in the code
**** first attempt to use partial gragments as replies to htmx requests
I've added logic for transposing the 2x1 card into 1x2 and vice-versa (as an exercise in using htmx).
Backing in "next orientation" into argument for the hx-get request, which is executed on click, and receives new html markup to get inserted, with the testimonial orientation classes changed.
*** Continued development
:PROPERTIES:
:CUSTOM_ID: continued-development
:END:
a better way to style the components, so that static file would also have the styling on the 'fragment' element
already found a way to render fragments without needing to put them into a separate file.
*** Useful resources
:PROPERTIES:
:CUSTOM_ID: useful-resources
:END:
- The htmx [[https://htmx.org/docs/][documentation]] and [[https://htmx.org/examples/][examples]]
- [[https://htmx.org/essays/][The articles on htmx approach to web dev]]
** Acknowledgments
:PROPERTIES:
:CUSTOM_ID: acknowledgments
:END:
Here I'll like to express gratitute to htmx, for writing examples and articles which helped me to go from totally not understanding their position, to actively wanting to learn this.

View File

@@ -0,0 +1,15 @@
ThisBuild / scalaVersion := "3.2.2"
fork := true
ThisBuild / version := "0.0.1"
ThisBuild / organization := "industries.sunshine"
lazy val testimonialsGrid = (project in file("."))
.settings(
name := "testimonials-grid-section",
libraryDependencies ++= Seq(
"com.lihaoyi" %% "cask" % "0.9.1",
"com.lihaoyi" %% "mainargs" % "0.5.0",
"org.thymeleaf" % "thymeleaf" % "3.1.1.RELEASE",
)
)

View File

@@ -0,0 +1,105 @@
{ pkgs, lib, sbt-derivation }:
let
pname = "testimonials-grid-section";
package = sbt-derivation.lib.mkSbtDerivation {
inherit pkgs pname;
# ...and the rest of the arguments
version = "0.0.1";
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
nativeBuildInputs = [ pkgs.nodePackages.tailwindcss ];
buildPhase = ''
sbt assembly
tailwindcss -i ./src/input.css -o ./output.css
'';
# css path different from ordinary development,
# because .gitignore makes it unavailable during nix build
# anyway copied to correct place
installPhase = ''
mkdir -p $out/bin
cp target/scala-*/${pname}-assembly-*.jar $out/bin/${pname}.jar
mkdir -p $out/bin/dist
cp ./output.css $out/bin/dist/output.css
cp -r public $out/bin/public
'';
depsSha256 = "sha256-Y5RktcE3fxUJci4o7LTuNlBEybTdVRqsG551AkVeRPw=";
};
in {
inherit package;
module = { config, pkgs, ... }:
let cfg = config.services.${pname};
in {
options.services.${pname} = {
enable = lib.mkEnableOption "My frontendmentor exercise ${pname}";
port = lib.mkOption {
type = lib.types.int;
default = 8080;
description = "Port to listen on.";
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "Host to bind to.";
};
useNginx = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to use Nginx to proxy requests.";
};
};
config = lib.mkIf cfg.enable {
users.groups."${pname}-group" = { };
users.users."${pname}-user" = {
isSystemUser = true;
group = "${pname}-group";
};
systemd.services.${pname} =
let serverHost = if cfg.useNginx then "localhost" else cfg.host;
in {
description = "Exercise app ${pname}";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
startLimitIntervalSec = 30;
startLimitBurst = 10;
serviceConfig = {
ExecStart =
"${pkgs.jdk}/bin/java -jar ${package}/bin/${pname}.jar -p ${
toString cfg.port
} --host ${serverHost}";
WorkingDirectory = "${package}/bin";
Restart = "on-failure";
User = "${pname}-user";
Group = "${pname}-group";
};
};
services.nginx = lib.mkIf cfg.useNginx {
virtualHosts.${cfg.host} = {
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
};
};
};
};
image = pkgs.dockerTools.buildLayeredImage {
name = pname;
tag = "latest";
created = "now";
config = {
Cmd = [
"${pkgs.jdk}/bin/java"
"-jar"
"${package}/bin/${pname}.jar"
"--host"
"0.0.0.0"
];
ExposedPorts = { "8080/tcp" = { }; };
WorkingDir = "${package}/bin";
};
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -0,0 +1 @@
sbt.version=1.9.0

View File

@@ -0,0 +1,3 @@
addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<svg width="104" height="102" xmlns="http://www.w3.org/2000/svg"><path d="M104 102V59.727H84.114c0-5.871.689-11.182 2.068-15.933 1.379-4.75 3.42-9.287 6.125-13.61C95.01 25.86 98.909 22.257 104 19.375V0c-9.758 4.27-17.712 9.874-23.864 16.813-6.151 6.939-10.712 14.545-13.681 22.818C63.485 47.904 62 59.941 62 75.74V102h42zm-62 0V59.727H22.114c0-5.871.689-11.182 2.068-15.933 1.379-4.75 3.42-9.287 6.125-13.61C33.01 25.86 36.909 22.257 42 19.375V0c-9.652 4.27-17.58 9.874-23.784 16.813C12.01 23.752 7.424 31.358 4.455 39.631 1.485 47.904 0 59.941 0 75.74V102h42z" fill="#A775F1" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

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

View File

@@ -0,0 +1,195 @@
<!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 href="/dist/output.css" rel="stylesheet" />
<link href="../../../../dist/output.css" rel="stylesheet" th:remove="all" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="public/images/favicon-32x32.png"
/>
<link href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@500;600&display=swap"
rel="stylesheet"
/>
<title>Frontend Mentor | [Challenge Name Here]</title>
<script type="text/javascript" src="public/deps/htmx.min.js"></script>
<!-- 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="grid grid-flow-col-dense place-content-center bg-light-grayish-blue">
<div class="py-20 px-7 space-y-7 min-h-screen text-white md:grid md:gap-[30px] md:py-0 md:px-0 h-max max-w-[1200px] md:grid-cols-[repeat(auto-fill,_255px)] md:auto-rows-[280px] md:mt-[160px] md:space-y-0">
<section
th:fragment="testimonialSection(testimonials)"
th:each="testimonial : ${testimonials}"
th:classappend="${testimonial.additionalSizeClasses} + ' ' + ${testimonial.additionalColorClasses}"
class="py-7 px-10 flex flex-col justify-between rounded-xl first:bg-[url('../public/images/bg-pattern-quotation.svg')] bg-no-repeat bg-[right_2rem_top] drop-shadow "
th:id="${testimonial.id}"
th:hx-get="'/testimonial/' + ${testimonial.id} + '?nextOrientation=' + ${testimonial.followupOrientation}"
hx-swap="outerHTML"
>
<div class="flex items-center">
<img
alt="Avatar image"
src="../../../../public/images/image-daniel.jpg"
th:src="${testimonial.avatarUrl}"
class="mr-5 w-10 h-10 rounded-full border-2 border-gray-400"
/>
<div class="grow">
<h1
th:text="${testimonial.author}"
>Daniel Clifford</h1>
<p class="text-sm opacity-50">Verified Graduate</p>
</div>
</div>
<h2 class="text-2xl"
th:text="${testimonial.header}"
>
I received a job offer mid-course, and the subjects I learned were
current, if not more so, in the company I joined. I honestly feel I
got every pennys worth.
</h2>
<p class="opacity-[70%] pb-2 leading-snug"
th:text="${testimonial.text}"
>
“ I was an EMT for many years before I joined the bootcamp. Ive been
looking to make a transition and have heard some people who had an
amazing experience here. I signed up for the free intro course and
found it incredibly fun! I enrolled shortly thereafter. The next 12
weeks was the best - and most grueling - time of my life. Since
completing the course, Ive successfully switched careers, working as
a Software Engineer at a VR startup. ”
</p>
</section>
<section
th:remove="all"
class="py-7 px-10 flex flex-col justify-between rounded-xl bg-very-dark-grayish-blue first:bg-[url('../public/images/bg-pattern-quotation.svg')] bg-no-repeat bg-[right_2rem_top] drop-shadow"
>
<div class="flex items-center">
<img
alt="Avatar image"
src="../../../../public/images/image-jonathan.jpg"
class="mr-5 w-10 h-10 rounded-full border-2 border-gray-400"
/>
<div class="grow">
<h1>Jonathan Walters</h1>
<p class="text-sm opacity-50">Verified Graduate</p>
</div>
</div>
<h2 class="text-2xl">
The team was very supportive and kept me motivated
</h2>
<p class="opacity-[70%] pb-2 leading-snug">
“ I started as a total newbie with virtually no coding skills. I now
work as a mobile engineer for a big company. This was one of the best
investments Ive made in myself. ”
</p>
</section>
<section
th:remove="all"
class="py-7 px-10 flex flex-col justify-between rounded-xl bg-white text-very-dark-blackish-blue first:bg-[url('../public/images/bg-pattern-quotation.svg')] bg-no-repeat bg-[right_2rem_top] drop-shadow"
>
<div class="flex items-center">
<img
alt="Avatar image"
src="../../../../public/images/image-jeanette.jpg"
class="mr-5 w-10 h-10 rounded-full border-2 border-gray-400"
/>
<div class="grow">
<h1>Jeanette Harmon</h1>
<p class="text-sm leading-snug opacity-50">Verified Graduate</p>
</div>
</div>
<h2 class="text-2xl">An overall wonderful and rewarding experience</h2>
<p class="opacity-[70%] pb-2 leading-snug">
“ Thank you for the wonderful experience! I now have a job I really
enjoy, and make a good living while doing something I love. ”
</p>
</section>
<section
th:remove="all"
class="py-7 px-10 flex flex-col justify-between rounded-xl bg-very-dark-blackish-blue text-white first:bg-[url('../public/images/bg-pattern-quotation.svg')] bg-no-repeat bg-[right_2rem_top] drop-shadow md:col-span-2"
>
<div class="flex items-center">
<img
alt="Avatar image"
src="../../../../public/images/image-patrick.jpg"
class="mr-5 w-10 h-10 rounded-full border-2 border-gray-400"
/>
<div class="grow">
<h1>Patrick Abrams</h1>
<p class="text-sm leading-snug opacity-50">Verified Graduate</p>
</div>
</div>
<h2 class="text-2xl">
Awesome teaching support from TAs who did the bootcamp themselves.
Getting guidance from them and learning from their experiences was
easy.
</h2>
<p class="opacity-[70%] pb-2 leading-snug">
“ The staff seem genuinely concerned about my progress which I find
really refreshing. The program gave me the confidence necessary to be
able to go out in the world and present myself as a capable junior
developer. The standard is above the rest. You will get the personal
attention you need from an incredible community of smart and amazing
people. ”
</p>
</section>
<section
th:remove="all"
class="py-7 px-10 flex flex-col justify-between rounded-xl bg-white text-very-dark-blackish-blue first:bg-[url('../public/images/bg-pattern-quotation.svg')] bg-no-repeat bg-[right_2rem_top] drop-shadow md:row-span-2 md:row-start-1 md:col-end-[-1]"
>
<div class="flex items-center">
<img
alt="Avatar image"
src="../../../../public/images/image-kira.jpg"
class="mr-5 w-10 h-10 rounded-full border-2 border-gray-400"
/>
<div class="grow">
<h1>Kira Whittle</h1>
<p class="text-sm opacity-50">Verified Graduate</p>
</div>
</div>
<h2 class="text-2xl">
Such a life-changing experience. Highly recommended!
</h2>
Quis hendrerit dolor magna eget est lorem ipsum dolor sit amet, consectetur adipiscing elit pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis! Nam libero justo, laoreet sit amet cursus sit amet, dictum sit amet justo donec.
<p class="opacity-[70%] pb-2 leading-snug">
“ Before joining the bootcamp, Ive never written a line of code. I
needed some structure from professionals who can help me learn
programming step by step. I was encouraged to enroll by a former
student of theirs who can only say wonderful things about the program.
The entire curriculum and staff did not disappoint. They were very
hands-on and I never had to wait long for assistance. The agile team
project, in particular, was outstanding. It took my learning to the
next level in a way that no tutorial could ever have. In fact, Ive
often referred to it during interviews as an example of my developent
experience. It certainly helped me land a job as a full-stack
developer after receiving multiple offers. 100% recommend! ”
</p>
</section>
</div>
<div class="fixed inset-x-0 bottom-0 attribution">
Challenge by
<a href="https://www.frontendmentor.io?ref=challenge" target="_blank"
>Frontend Mentor</a
>. Source code at <a href="https://github.com/efim/Learning-HTMX">Your Name Here</a>.
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
<section
th:replace="index::testimonialSection(testimonials=${selectedTestimonials})">
</section>

View File

@@ -0,0 +1,86 @@
package testimonialsgrid
import mainargs.{main, arg, ParserForMethods}
import cask.main.Routes
import org.thymeleaf.context.Context
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
import org.thymeleaf.TemplateEngine
import scala.jdk.CollectionConverters._
object Main {
@main def run(
@arg(
name = "port",
short = 'p',
doc = "Port on which server will start serving."
)
portArg: Int = 8080,
@arg(name = "host", doc = "Host on which server will start serving.")
hostArg: String = "localhost"
): Unit = {
println(s"Will start server on ${hostArg}:${portArg}")
val server = new cask.Main {
override def allRoutes: Seq[Routes] = Seq(AppRoutes())
override def port: Int = portArg
override def host: String = hostArg
}
server.main(Array.empty)
}
def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
case class AppRoutes()(implicit cc: castor.Context, log: cask.Logger)
extends cask.Routes {
val templateResolver = new ClassLoaderTemplateResolver()
templateResolver.setPrefix("templates/");
templateResolver.setSuffix(".html")
templateResolver.setTemplateMode("HTML5")
val templateEngine = new TemplateEngine()
templateEngine.setTemplateResolver(templateResolver)
@cask.get("/")
def index() = {
val context = new Context()
context.setVariable(
"testimonials",
Testimonial.sameAsRequested.asJava
)
val result = templateEngine.process("index", context)
cask.Response(
result,
headers = Seq("Content-Type" -> "text/html;charset=UTF-8")
)
}
@cask.get("/testimonial/:id")
def getTestimonial(id: String, nextOrientation: Int) = {
// println(s"got params $nextOrientation")
val context = new Context()
// wow, i need copy because attributes are vars and not vals,
// didn't have to think about this in a long long time
val foundTestimonial =
Testimonial.sameAsRequested.find(_.id == id).get.copy()
foundTestimonial.setNextSizeClass(nextOrientation)
// println(
// s"should change size and orientation : $foundTestimonial "
// )
context.setVariable("selectedTestimonials", List(foundTestimonial).asJava)
val result = templateEngine.process("testimonialSection", context)
cask.Response(
result,
headers = Seq("Content-Type" -> "text/html;charset=UTF-8")
)
}
@cask.staticFiles("/dist")
def distFiles() = "dist"
@cask.staticFiles("/public")
def publicFiles() = "public"
initialize()
}
}

View File

@@ -0,0 +1,142 @@
package testimonialsgrid
import scala.beans.BeanProperty
import java.util.UUID
import scala.util.Random
final case class Testimonial(
@BeanProperty var avatarUrl: String,
@BeanProperty var author: String,
@BeanProperty var header: String,
@BeanProperty var text: String,
@BeanProperty var id: String,
@BeanProperty var additionalColorClasses: String,
@BeanProperty var additionalSizeClasses: String,
@BeanProperty var followupOrientation: Int = 0,
sequentSizeClasses: List[String] = List("")
) {
def setNextSizeClass(key: Int): Unit = {
val nextOrientationIndex = (key + 1) % sequentSizeClasses.size
this.additionalSizeClasses = sequentSizeClasses(nextOrientationIndex)
this.followupOrientation = nextOrientationIndex
}
}
object Testimonial {
val colorful = List(
new Testimonial(
"public/images/image-patrick.jpg",
"Leopold",
" Odio facilisis mauris sit amet massa vitae tortor condimentum lacinimport java.util.UUID ia quis vel eros donec ac odio tempor orci dapibus ultrices. ",
" Nibh sed pulvinar proin gravida hendrerit? Massa tincidunt nunc pulvinar sapien et ligula libero nunc!",
UUID.randomUUID().toString(),
"bg-red-500",
""
),
new Testimonial(
"public/images/image-jonathan.jpg",
"Aragorn",
" Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum! ",
" Egestas fringilla phasellus faucibus scelerisque eleifend! Dignissim enim, sit amet venenatis urna cursus eget nunc scelerisque viverra mauris, in aliquam sem fringilla ut morbi tincidunt augue interdum velit euismod in! ",
UUID.randomUUID().toString(),
"bg-blue-500",
"md:col-span-2"
),
new Testimonial(
"public/images/image-jeanette.jpg",
"Jeanatte Mamamia",
" Id venenatis a, condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas sed tempus, urna et pharetra pharetra! ",
" Amet nulla facilisi morbi tempus iaculis urna, id volutpat lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Lorem dolor, sed viverra ipsum nunc aliquet bibendum? ",
UUID.randomUUID().toString(),
"bg-green-200 text-black",
"md:row-span-2"
),
new Testimonial(
"public/images/image-kira.jpg",
"Mamma mia",
" Id venenatis a, condimentum vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas sed tempus, urna et pharetra pharetra! ",
""" Quis hendrerit dolor magna eget est lorem ipsum dolor sit amet, consectetur adipiscing elit pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis! Nam libero justo, laoreet sit amet cursus sit amet, dictum sit amet justo donec.
""",
UUID.randomUUID().toString(),
"bg-amber-200 text-black",
"md:col-span-2"
)
)
val sameAsRequested = List(
new Testimonial(
"public/images/image-daniel.jpg",
"Daniel Clifford",
"""I received a job offer mid-course, and the subjects I learned were current, if not more so,
in the company I joined. I honestly feel I got every pennys worth.
""",
""" “ I was an EMT for many years before I joined the bootcamp. Ive been looking to make a
transition and have heard some people who had an amazing experience here. I signed up
for the free intro course and found it incredibly fun! I enrolled shortly thereafter.
The next 12 weeks was the best - and most grueling - time of my life. Since completing
the course, Ive successfully switched careers, working as a Software Engineer at a VR startup. ”
""",
UUID.randomUUID().toString(),
"bg-moderate-violet",
"md:col-span-2",
sequentSizeClasses = List("md:row-span-2", "md:col-span-2")
),
new Testimonial(
"public/images/image-jonathan.jpg",
"Jonathan Walters",
" The team was very supportive and kept me motivated ",
""" " Egestas fringilla phasellus faucibus scelerisque eleifend! Dignissim enim, sit amet venenatis urna cursus eget nunc scelerisque viverra mauris, in aliquam sem fringilla ut morbi tincidunt augue interdum velit euismod in! ",
""",
UUID.randomUUID().toString(),
"bg-very-dark-grayish-blue",
"md:col-span-1"
),
new Testimonial(
"public/images/image-jeanette.jpg",
"Jeanette Harmon",
"An overall wonderful and rewarding experience",
""" “ Thank you for the wonderful experience! I now have a job I really enjoy, and make a good living
while doing something I love. ”
""",
UUID.randomUUID().toString(),
"bg-white text-very-dark-blackish-blue",
"md:col-span-1"
),
new Testimonial(
"public/images/image-patrick.jpg",
"Patrick Abrams",
""" " Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum! ",
""",
""" “ The staff seem genuinely concerned about my progress which I find really refreshing. The program
gave me the confidence necessary to be able to go out in the world and present myself as a capable
junior developer. The standard is above the rest. You will get the personal attention you need from
an incredible community of smart and amazing people. ”
""",
UUID.randomUUID().toString(),
"bg-very-dark-blackish-blue",
"md:col-span-2",
sequentSizeClasses = List("md:row-span-2", "md:col-span-2")
),
new Testimonial(
"public/images/image-kira.jpg",
"Kira Whittle",
" Such a life-changing experience. Highly recommended! ",
""" “ Before joining the bootcamp, Ive never written a line of code. I
needed some structure from professionals who can help me learn
programming step by step. I was encouraged to enroll by a former
student of theirs who can only say wonderful things about the program.
The entire curriculum and staff did not disappoint. They were very
hands-on and I never had to wait long for assistance. The agile team
project, in particular, was outstanding. It took my learning to the
next level in a way that no tutorial could ever have. In fact, Ive
often referred to it during interviews as an example of my developent
experience. It certainly helped me land a job as a full-stack
developer after receiving multiple offers. 100% recommend! ”
""",
UUID.randomUUID().toString(),
"bg-white text-very-dark-blackish-blue",
"md:row-span-2 md:col-end-[-1] md:row-start-1",
sequentSizeClasses = List("md:row-span-2","md:col-span-2")
)
)
}

View File

@@ -0,0 +1,8 @@
package example
class ExampleSuite extends munit.FunSuite:
test("addition") {
assert(1 + 1 == 2)
}
end ExampleSuite

View File

@@ -0,0 +1,38 @@
# Front-end Style Guide
## Layout
The designs were created to the following widths:
- Mobile: 375px
- Desktop: 1440px
## Colors
### Primary
Moderate violet: hsl(263, 55%, 52%)
Very dark grayish blue: hsl(217, 19%, 35%)
Very dark blackish blue: hsl(219, 29%, 14%)
White: hsl(0, 0%, 100%)
### Neutral
Light gray: hsl(0, 0%, 81%)
Light grayish blue: hsl(210, 46%, 95%)
Note for text colors:
1. "Verified Graduate" has the same color as the person's name with 50% opacity
2. Review paragraphs inside the quotations have the same color as well, but are at 70% opacity
## Typography
### Body Copy
- Font size: 13px
### Font
- Family: [Barlow Semi Condensed](https://fonts.google.com/specimen/Barlow+Semi+Condensed)
- Weights: 500, 600

View File

@@ -0,0 +1,24 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{html,scala}"],
theme: {
extend: {
colors: {
'moderate-violet': 'hsl(263, 55%, 52%)',
'very-dark-grayish-blue': 'hsl(217, 19%, 35%)',
'very-dark-blackish-blue': 'hsl(219, 29%, 14%)',
'light-gray': 'hsl(0, 0%, 81%)',
'light-grayish-blue': 'hsl(210, 46%, 95%)',
},
fontFamily: {
'sans': ['Barlow Semi Condensed', 'sans-serif'], // This will set Roboto as the default sans font
},
fontWeight: {
'normal': 400,
'bold': 500,
}
},
},
plugins: [],
}

16
14-rock-paper-scissors/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
.bsp/
.scala-build/
.metals/
.direnv
*/dist/
/11-single-price-grid-component/.bloop/
**/.bloop
**/project/project/
**/project/metals.sbt
**/project/.bloop
**/project/target/
**/target/
*/result
result

View File

View File

@@ -0,0 +1,2 @@
version = "3.7.3"
runner.dialect = scala3

View File

@@ -0,0 +1,166 @@
* Frontend Mentor - Rock, Paper, Scissors solution
:PROPERTIES:
:CUSTOM_ID: frontend-mentor---rock-paper-scissors-solution
:END:
This is a solution to the
[[https://www.frontendmentor.io/challenges/rock-paper-scissors-game-pTgwgvgH][Rock,
Paper, Scissors 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 game depending on their device's
screen size
- Play Rock, Paper, Scissors against the computer
- Maintain the state of the score after refreshing the browser
/(optional)/
- *Bonus*: Play Rock, Paper, Scissors, Lizard, Spock against the
computer /(optional)/
*** Screenshot
:PROPERTIES:
:CUSTOM_ID: screenshot
:END:
[[screenshot-desktop.png]]
[[screenshot-mobile.png]]
*** Links
:PROPERTIES:
:CUSTOM_ID: links
:END:
- Solution URL
- [[https://efim-frontendmentor-rock-paper-scissors.onrender.com/][Live Site URL]]
** My process
:PROPERTIES:
:CUSTOM_ID: my-process
:END:
*** Built with
:PROPERTIES:
:CUSTOM_ID: built-with
:END:
- Semantic HTML5 markup
- TailwindCSS, css animations
- Flexbox & CSS Grid
- Mobile-first workflow
- SSR on Scala with Cask
- Thymeleaf templates
- htmx for partial page updates and interactivity
*** What I learned
:PROPERTIES:
:CUSTOM_ID: what-i-learned
:END:
**** for template fragement styling - using CSS vars in <style> tag
Allows for "initial" fragment specification to have static styling for viewing the page directly.
This is useful for fragments that should have different stylings, like hand selection badges - should have different colors, so colors are specified in the code and passed as css var values via "th:style".
Ordinary "style" attribute allows the tag which is marked by "th:fragment" to be viewed with some default styles. This is needed because for the "static" view of the page, browser ignores "th:fragment" attribute and just renders what it knows, as well as 'paper' and 'rock' badges, which are marked by "th:remove='all'" tag, which means they are only present in the "mockup static view"
But! Syling sizes this way seems to be an error, i don't want to specify 8rem in my code for the fragments, and that also makes styling of responsive design complicated. I guess I'll want the fragment itself occupy "all parent" and control the size of parent from html where the fragment is inserted.
**** different htmx controls:
***** the hx-get on the click
substitutes the hand selection part of the page to the initial "showdown" - with selected hand and animated wait on the "house choice".
***** on load + delay:3s hx-get
on "wait for house choice" fragemnt
means the "get house choice" rest method executed automatically, and generates random choice.
I'm substituting both hands \ whole 'showdown table' so i'm passing also a players choice into '/house-choice' rest endpoint.
I could only substitue the house choice badge and the message, that would have been a simpler design.
***** i wanted for message to show up with delay
so initially i though I'll do another timed on load request to fetch message, but figured that i could use css animation to fade the message in.
***** handling state of the fragment
Creating a scala object "ShowdownState" allows for setting single variable into context, and then at least "having all attributes of state" is enforced by scala compiler.
In documentaion i found that there's a shorthand for referencing attributes of single object:
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expressions-on-selections-asterisk-syntax
So i could set "th:object="${showdownState}"
and then reference directly it's attributes by "*{playersChoice}"
This can mitigate untyped nature of template variables.
***** I got more experience with laying out template fragments.
Putting 'showdown table' into separate file definitely helped, styling in the opened static file is nice.
I'm not sure how much separate files are necessary. Maybe state of "player hand is present, house hand is not present" separate of "showdown and both hands are present" would be easier for styling. Because fragments could be shown at the page.
Negative side of that - all other "not for render" parts of the page would have to be styled and kept in sync with the parts of pages for styling.
Maybe I'll do put some bigger fragments into separate files, but not recrete the outer page for them, just keep them in center of blank page.
***** Triggering client events from HTMX
https://htmx.org/headers/hx-trigger/
Adding header to REST response will trigger js event in the page.
#+begin_src scala
cask.Response(
result,
headers = Seq(
"Content-Type" -> "text/html;charset=UTF-8",
"HX-Trigger-After-Settle" -> s"""{"updateScore": ${showdownState.scoreChange}}"""
)
)
#+end_src
This is a way to pass data from server into js code, executing on client.
For exmaple +1 \ +0 \ -1 for the score change.
***** Using small js scripts for browser functions
I.e updating score, saving it into local storage and loading.
Two simple scripts directly near the html markup which contains the score:
#+begin_src html
<script type="text/javascript">
document.body.addEventListener("updateScore", function (evt) {
let scoreElement = document.querySelector("#the-score-number");
let newScore =
parseInt(scoreElement.textContent) + evt.detail.value;
console.log(
`the score will update by ${evt.detail.value} to ${newScore}`
);
localStorage.setItem("score", newScore);
scoreElement.textContent = newScore;
});
</script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", (event) => {
let scoreElement = document.querySelector("#the-score-number");
let storedScore = localStorage.getItem("score");
if (storedScore !== null) {
scoreElement.textContent = storedScore;
} else {
scoreElement.textContent = 0;
localStorage.setItem("score", 0);
}
});
</script>
#+end_src
And debugging directly in the static preview.
We can create event in the console and fire it from any element:
#+begin_src js
var myEvent2 = new CustomEvent('updateScore', {detail : {value: -1}});
document.body.dispatchEvent(myEvent2)
#+end_src
*** Continued development
:PROPERTIES:
:CUSTOM_ID: continued-development
:END:
I could remake the html, to take into account the desktop layout. Which i didn't plan out and just didn't do - right now desktop only shows mobile layout increased in size.
Overall in the future I'd want to practice more with features available in htmx, to know how to make websites with interactivity expected my modern users.
And also - practice integration with js libraries - htmx examples show integration with sortable via events, and many others can be possible.

View File

@@ -0,0 +1,15 @@
ThisBuild / scalaVersion := "3.2.2"
fork := true
ThisBuild / version := "0.0.1"
ThisBuild / organization := "industries.sunshine"
lazy val rockPaperScissors = (project in file("."))
.settings(
name := "rock-paper-scissors",
libraryDependencies ++= Seq(
"com.lihaoyi" %% "cask" % "0.9.1",
"com.lihaoyi" %% "mainargs" % "0.5.0",
"org.thymeleaf" % "thymeleaf" % "3.1.1.RELEASE",
)
)

View File

@@ -0,0 +1,104 @@
{ pkgs, lib, sbt-derivation }:
let
pname = "rock-paper-scissors";
package = sbt-derivation.lib.mkSbtDerivation {
inherit pkgs pname;
# ...and the rest of the arguments
version = "0.0.1";
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
nativeBuildInputs = [ pkgs.nodePackages.tailwindcss ];
buildPhase = ''
tailwindcss -i ./src/input.css -o ./src/main/resources/public/output.css
sbt assembly
'';
# css path different from ordinary development,
# because .gitignore makes it unavailable during nix build
# anyway copied to correct place
installPhase = ''
mkdir -p $out/bin
cp target/scala-*/${pname}-assembly-*.jar $out/bin/${pname}.jar
'';
depsSha256 = "sha256-Y5RktcE3fxUJci4o7LTuNlBEybTdVRqsG551AkVeRPw=";
};
module = { config, pkgs, ... }:
let cfg = config.services.${pname};
in {
options.services.${pname} = {
enable = lib.mkEnableOption "My frontendmentor exercise ${pname}";
port = lib.mkOption {
type = lib.types.int;
default = 8080;
description = "Port to listen on.";
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "Host to bind to.";
};
useNginx = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to use Nginx to proxy requests.";
};
};
config = lib.mkIf cfg.enable {
users.groups."${pname}-group" = { };
users.users."${pname}-user" = {
isSystemUser = true;
group = "${pname}-group";
};
systemd.services.${pname} =
let serverHost = if cfg.useNginx then "localhost" else cfg.host;
in {
description = "Exercise app ${pname}";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
startLimitIntervalSec = 30;
startLimitBurst = 10;
serviceConfig = {
ExecStart =
"${pkgs.jdk}/bin/java -jar ${package}/bin/${pname}.jar -p ${
toString cfg.port
} --host ${serverHost}";
WorkingDirectory = "${package}/bin";
Restart = "on-failure";
User = "${pname}-user";
Group = "${pname}-group";
};
};
services.nginx = lib.mkIf cfg.useNginx {
virtualHosts.${cfg.host} = {
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
};
};
};
};
image = pkgs.dockerTools.buildLayeredImage {
name = pname;
tag = "latest";
created = "now";
config = {
Cmd = [ "${pkgs.jdk}/bin/java" "-jar" "${package}/bin/${pname}.jar" "--host" "0.0.0.0" ];
ExposedPorts = {
"8080/tcp" = {};
};
};
};
# image = pkgs.dockerTools.buildLayeredImage { # so, wow, this works
# name = "hello2";
# tag = "latest";
# config.Cmd = [ "${pkgs.hello}/bin/hello" ];
# };
in {
package = package;
module = module;
image = image;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Some files were not shown because too many files have changed in this diff Show More