Compare commits

..

169 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
282 changed files with 11370 additions and 220 deletions

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

@@ -1,88 +0,0 @@
# Frontend Mentor - Single price grid component
![Design preview for the Single price grid component coding challenge](./design/desktop-preview.jpg)
## Welcome! 👋
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 basic understanding of HTML and CSS.**
## The challenge
Your 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
Want some support on the challenge? [Join our Slack community](https://www.frontendmentor.io/slack) 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. 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).
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.
## Building your project
Feel free to use any workflow that you feel comfortable with. Below is a suggested process, but do not feel like you need to follow these steps:
1. Initialize your project as a public repository on [GitHub](https://github.com/). Creating a repo will make it easier to share your code with the community if you need help. If you're not sure how to do this, [have a read-through of this Try Git resource](https://try.github.io/).
2. Configure your repository to publish your code to a web address. This will also be useful if you need some help during a challenge as you can share the URL for your project with your repo URL. There are a number of ways to do this, and we provide some recommendations below.
3. Look through the designs to start planning out how you'll tackle the project. This step is crucial to help you think ahead for CSS classes to create reusable styles.
4. Before adding any styles, structure your content with HTML. Writing your HTML first can help focus your attention on creating well-structured content.
5. Write out the base styles for your project, including general content styles, such as `font-family` and `font-size`.
6. Start adding styles to the top of the page and work down. Only move on to the next section once you're happy you've completed the area you're working on.
## Deploying your project
As mentioned above, there are many ways to host your project for free. Our recommend hosts are:
- [GitHub Pages](https://pages.github.com/)
- [Vercel](https://vercel.com/)
- [Netlify](https://www.netlify.com/)
You can host your site using one of these solutions or any of our other trusted providers. [Read more about our recommended and trusted hosts](https://medium.com/frontend-mentor/frontend-mentor-trusted-hosting-providers-bf000dfebe).
## Create a custom `README.md`
We strongly recommend overwriting this `README.md` with a custom one. We've provided a template inside the [`README-template.md`](./README-template.md) file in this starter code.
The template provides a guide for what to add. A custom `README` will help you explain your project and reflect on your learnings. Please feel free to edit our template as much as you like.
Once you've added your information to the template, delete this file and rename the `README-template.md` file to `README.md`. That will make it show up as your repository's README file.
## Submitting your solution
Submit your solution on the platform for the rest of the community to see. Follow our ["Complete guide to submitting solutions"](https://medium.com/frontend-mentor/a-complete-guide-to-submitting-solutions-on-frontend-mentor-ac6384162248) for tips on how to do this.
Remember, if you're looking for feedback on your solution, be sure to ask questions when submitting it. The more specific and detailed you are with your questions, the higher the chance you'll get valuable feedback from the community.
## Sharing your solution
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).
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/).
We provide templates to help you share your solution once you've submitted it on the platform. Please do edit them and include specific questions when you're looking for feedback.
The more specific you are with your questions the more likely it is that another member of the community will give you feedback.
## Got feedback for us?
We love receiving feedback! We're always looking to improve our challenges and our platform. So if you have anything you'd like to mention, please email hi[at]frontendmentor[dot]io.
This challenge is completely free. Please share it with anyone who will find it useful for practice.
**Have fun building!** 🚀

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

@@ -4,7 +4,7 @@ fork := true
ThisBuild / version := "0.0.1"
ThisBuild / organization := "industries.sunshine"
lazy val root = (project in file("."))
lazy val singlePriceGridComponent = (project in file("."))
.settings(
name := "priceGrid",

View File

@@ -1,10 +1,10 @@
{ pkgs, lib, sbt-derivation }:
let
pname = "price-grid-app";
package = sbt-derivation.lib.mkSbtDerivation {
inherit pkgs;
inherit pkgs pname;
# ...and the rest of the arguments
pname = "price-grid-app";
version = "0.0.1";
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
nativeBuildInputs = [ pkgs.nodePackages.tailwindcss ];
@@ -84,7 +84,35 @@ let
};
};
};
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 {
package = package;
module = module;
inherit package module image;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -128,8 +128,8 @@ object Page {
target := "_blank",
"Frontend Mentor. "
),
"Coded by ",
a(href := "#", "Your Name Here")
"Source code at",
a(href := "https://github.com/efim/Learning-HTMX", "Your Name Here")
)
)
}

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

@@ -1,110 +0,0 @@
# Frontend Mentor - Order summary card solution
This is a solution to the [Order summary card challenge on Frontend Mentor](https://www.frontendmentor.io/challenges/order-summary-component-QlPmajDUj). Frontend Mentor challenges help you improve your coding skills by building realistic projects.
## Table of contents
- [Overview](#overview)
- [The challenge](#the-challenge)
- [Screenshot](#screenshot)
- [Links](#links)
- [My process](#my-process)
- [Built with](#built-with)
- [What I learned](#what-i-learned)
- [Continued development](#continued-development)
- [Useful resources](#useful-resources)
- [Author](#author)
- [Acknowledgments](#acknowledgments)
**Note: Delete this note and update the table of contents based on what sections you keep.**
## Overview
### The challenge
Users should be able to:
- See hover states for interactive elements
### Screenshot
![](./screenshot.jpg)
Add a screenshot of your solution. The easiest way to do this is to use Firefox to view your project, right-click the page and select "Take a Screenshot". You can choose either a full-height screenshot or a cropped one based on how long the page is. If it's very long, it might be best to crop it.
Alternatively, you can use a tool like [FireShot](https://getfireshot.com/) to take the screenshot. FireShot has a free option, so you don't need to purchase it.
Then crop/optimize/edit your image however you like, add it to your project, and update the file path in the image above.
**Note: Delete this note and the paragraphs above when you add your screenshot. If you prefer not to add a screenshot, feel free to remove this entire section.**
### Links
- Solution URL: [Add solution URL here](https://your-solution-url.com)
- Live Site URL: [Add live site URL here](https://your-live-site-url.com)
## My process
### Built with
- Semantic HTML5 markup
- CSS custom properties
- Flexbox
- CSS Grid
- Mobile-first workflow
- [React](https://reactjs.org/) - JS library
- [Next.js](https://nextjs.org/) - React framework
- [Styled Components](https://styled-components.com/) - For styles
**Note: These are just examples. Delete this note and replace the list above with your own choices**
### What I learned
Use this section to recap over some of your major learnings while working through this project. Writing these out and providing code samples of areas you want to highlight is a great way to reinforce your own knowledge.
To see how you can add code snippets, see below:
```html
<h1>Some HTML code I'm proud of</h1>
```
```css
.proud-of-this-css {
color: papayawhip;
}
```
```js
const proudOfThisFunc = () => {
console.log('🎉')
}
```
If you want more help with writing markdown, we'd recommend checking out [The Markdown Guide](https://www.markdownguide.org/) to learn more.
**Note: Delete this note and the content within this section and replace with your own learnings.**
### Continued development
Use this section to outline areas that you want to continue focusing on in future projects. These could be concepts you're still not completely comfortable with or techniques you found useful that you want to refine and perfect.
**Note: Delete this note and the content within this section and replace with your own plans for continued development.**
### Useful resources
- [Example resource 1](https://www.example.com) - This helped me for XYZ reason. I really liked this pattern and will use it going forward.
- [Example resource 2](https://www.example.com) - This is an amazing article which helped me finally understand XYZ. I'd recommend it to anyone still learning this concept.
**Note: Delete this note and replace the list above with resources that helped you during the challenge. These could come in handy for anyone viewing your solution or for yourself when you look back on this project in the future.**
## Author
- Website - [Add your name here](https://www.your-site.com)
- Frontend Mentor - [@yourusername](https://www.frontendmentor.io/profile/yourusername)
- Twitter - [@yourusername](https://www.twitter.com/yourusername)
**Note: Delete this note and add/remove/edit lines above based on what links you'd like to share.**
## Acknowledgments
This is where you can give a hat tip to anyone who helped you out on this project. Perhaps you worked in a team or got some inspiration from someone else's solution. This is the perfect place to give them some credit.
**Note: Delete this note and edit this section's content as necessary. If you completed this challenge by yourself, feel free to delete this section entirely.**

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

@@ -8,7 +8,7 @@ val toolkitV = "0.1.7"
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV
lazy val root = (project in file("."))
lazy val orderSummaryComponent = (project in file("."))
.settings(
name := "order-summary-component",
libraryDependencies += toolkit,

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";
};
};
}

View File

@@ -1 +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: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -43,7 +43,7 @@
Challenge by
<a href="https://www.frontendmentor.io?ref=challenge" target="_blank"
>Frontend Mentor</a
>. Coded by <a href="#">Your Name Here</a>.
>. Source code at <a href="https://github.com/efim/Learning-HTMX">Your Name Here</a>.
</div>
</body>
</html>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 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: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -0,0 +1,53 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* div, section, span, p, h1, button { */
/* outline: 1px solid red; */
/* } */
@keyframes slowly-appear {
0% {
opacity: 0;
}
30% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.slowly-appear {
animation: slowly-appear 3s;
}
@keyframes pulsing-animation {
0% {
transform: scale(0);
}
100% {
transform: scale(2);
opacity: 0;
}
}
.pulsing-animation {
animation: pulsing-animation 1s;
animation-iteration-count: 3;
transform-origin: center;
}
@keyframes quickly-appear {
0% {
opacity: 0;
}
100% {
opacity: 0.05;
}
}
.quickly-appear {
animation: quickly-appear 2s;
transform-origin: center;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="329" height="313"><path fill="none" stroke="#000" stroke-width="15" d="M164.5 9.27L9.26 122.06l59.296 182.495h191.888L319.74 122.06 164.5 9.271z" opacity=".2"/></svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@@ -0,0 +1 @@
<svg width="313" height="278" xmlns="http://www.w3.org/2000/svg"><path stroke="#000" stroke-width="15" fill="none" opacity=".2" d="M156.5 262 300 8H13z"/></svg>

After

Width:  |  Height:  |  Size: 160 B

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"><path fill="#3B4262" fill-rule="evenodd" d="M16.97 0l2.122 2.121-7.425 7.425 7.425 7.425-2.121 2.12-7.425-7.424-7.425 7.425L0 16.97l7.425-7.425L0 2.121 2.121 0l7.425 7.425L16.971 0z" opacity=".25"/></svg>

After

Width:  |  Height:  |  Size: 267 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="63" height="60"><path fill="#3B4262" d="M49.483 2.096c3.229-2 6.324-2.433 8.714-1.219 5.359 2.72 4.583 12.845 2.39 17.232-1.238 2.477-3.432 4.287-6.527 5.387-1.2 3.799-2.884 7.064-5.034 9.764a7.904 7.904 0 005.079 1.837h.09l3.02-2.982c.761-.75 1.994-.751 2.754 0 .76.75.76 1.968 0 2.718l-.267.264h.608c1.075 0 1.947.861 1.947 1.923 0 1.061-.872 1.922-1.947 1.922h-.608l.267.264c.76.75.76 1.968 0 2.719-.38.375-.878.563-1.376.563-.499 0-.997-.188-1.377-.563l-3.022-2.983h-.09a11.823 11.823 0 01-7.724-2.857c-.415.377-.843.738-1.284 1.083-3.732 2.92-8.294 4.617-13.627 5.082a12.08 12.08 0 01.343 6.352l-.02.09 2.325 3.66a1.998 1.998 0 01-.589 2.741 1.936 1.936 0 01-1.057.316 1.953 1.953 0 01-1.651-.912l-.206-.324-.13.605a1.966 1.966 0 01-1.913 1.562 1.95 1.95 0 01-.419-.046 1.984 1.984 0 01-1.498-2.36l.13-.605-.32.209a1.946 1.946 0 01-2.709-.597 1.998 1.998 0 01.59-2.74l3.617-2.353.02-.09a8.073 8.073 0 00-.713-5.394 44.773 44.773 0 01-4.797-.4c-4.684-.634-7.341 1.433-8.044 2.081-5.518 5.093-6.586 14.092-6.596 14.183a1.977 1.977 0 01-1.96 1.757 1.97 1.97 0 01-1.63-.867c-.087-.13-2.164-3.234-3.075-7.615-1.237-5.95.238-11.407 4.266-15.783a20.702 20.702 0 018.738-5.615l-.407.106a7.99 7.99 0 00-3.375-.747h-.09l-3.032 2.983a1.965 1.965 0 01-1.382.563c-.5 0-1-.188-1.382-.563a1.9 1.9 0 010-2.719l.268-.264h-.61c-1.08 0-1.954-.86-1.954-1.922 0-1.062.875-1.922 1.954-1.922h.61l-.268-.264a1.9 1.9 0 010-2.72c.763-.75 2-.75 2.764 0l3.032 2.983h.09a11.87 11.87 0 018.661 3.726l-.217-.223a22.914 22.914 0 015.352.095c4.946.67 8.99-.018 12.113-2.052a12.215 12.215 0 01-2.71-7.7v-.092L30.6 16.287a2.034 2.034 0 010-2.812 1.885 1.885 0 012.725 0l.265.273v-.621c0-1.099.862-1.989 1.926-1.989s1.927.89 1.927 1.989v.62l.265-.272a1.885 1.885 0 012.725 0c1.146 1.183.32 2.483 0 2.812l-2.99 3.086v.091c0 1.878.635 3.673 1.771 5.098 1.412-1.686 2.522-3.808 3.325-6.36-.587-1.625-1.5-5.473.828-9.837 1.22-2.285 3.564-4.687 6.117-6.269z"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="49" height="59"><path fill="#3B4262" d="M47.125 11.832a2.922 2.922 0 00-1.232-.198c-.57.04-1.029.271-1.302.65-1.604 2.248-2.919 6.493-3.979 9.905-.486 1.577-1.14 3.688-1.612 4.69-.493-2.807.064-13.09.28-17.05l.003-.064c.15-2.751.17-3.234.138-3.446-.238-1.509-.843-2.5-1.799-2.943-.966-.45-2.22-.25-3.572.563-.677.41-.865 1.816-1.446 8.19l-.002.028c-.32 3.502-1.058 11.566-1.965 12.91-1.023-1.88-2.431-12.555-3.039-17.176-.425-3.236-.673-5.094-.84-5.655-.35-1.176-1.83-2.176-3.295-2.232-1.22-.06-2.22.56-2.698 1.638-.894.995-.578 4.292.41 12.102.47 3.718 1.44 11.395.83 12.257-1.219-.133-3.31-4.942-6.215-14.299-.816-2.62-1.068-3.408-1.318-3.753-.494-1.202-2.172-2.129-3.676-2.024a3.183 3.183 0 00-.377.049c-.787.156-2.584.881-2.2 4.226 1.06 4.637 2.213 8.041 3.331 11.346l.023.066c.669 1.98 1.302 3.85 1.89 5.925 1.385 4.9.846 7.94.84 7.975-.046.312-.143.503-.288.57a.556.556 0 01-.195.045c-.44.03-1.098-.26-1.437-.45-.776-1.482-4.636-8.544-8.134-9.524l-.126-.037-.127.012c-1.283.121-2.226.606-2.803 1.441-.914 1.32-.535 3.002-.444 3.34l.052.12c.028.051 2.834 5.165 3.268 7.544.374 2.04 2.311 4.25 3.869 6.026l.064.073c.508.58.946 1.083 1.292 1.548 4.519 4.713 11.665 8.677 11.723 8.71.892.657 1.387 1.293 1.44 1.84a.798.798 0 01-.16.58l-.155.162.988.96 18.853-1.324.804-3.684c2.486-10.402 1.967-19.272 1.958-19.33.01-.327.706-3.483 1.266-6.033l.017-.065c1.117-5.08 2.505-11.4 2.772-13.803.116-1.028-.542-1.972-1.675-2.401z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path fill="#3B4262" d="M45.06 12.22c-.642-8.096-9.734-7.269-9.734-7.269-3.837-6.765-9.832-1.865-9.832-1.865-4.606-6.63-10.38-.486-10.38-.486-9.957-1.074-9.571 7.066-9.571 7.066-.234 2.588 1.403 10.593 1.403 10.593-1.477-4.614-4.68-.784-4.68-.784-3.94 6.078-.975 9.405-.975 9.405 5.33 6.246 16.688 13.743 16.688 13.743 4.113 2.356 2.373 4.457 2.373 4.457l24.876-4.11.571-4.718c3.782-11.436-.739-26.032-.739-26.032z"/></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="51" height="58"><path fill="#3B4262" d="M13.971 25.702l6.012-8.415c-2.499-.415-7.088-.507-10.846 3.235C3.212 26.421.812 39.163.312 42.248L15.37 57.24c2.711-.232 14.713-1.827 26.279-13.34.122-.249 2.94-2.321.636-4.614-1.1-1.095-2.919-1.074-4.042.044-.572.57-1.461.577-2.021.02-.56-.557-.552-1.443.02-2.012l4.087-4.069c2.076-2.067.119-5.555-2.78-4.717l-3.345 2.851c-.611.53-1.52.439-2.022-.14-.519-.597-.408-1.503.183-2.013 11.687-10.208 9.98-8.979 17.5-15.995 2.809-2.329-.725-6.447-3.493-4.09L28.182 25.45c-.529.448-1.34.457-1.86-.02-.601-.517-.615-1.262-.222-1.85L38.787 3.944c1.854-2.5-1.795-5.277-3.749-2.757L16.28 27.307c-.452.65-1.364.8-1.985.345a1.377 1.377 0 01-.323-1.95z"/></svg>

After

Width:  |  Height:  |  Size: 735 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="59"><path fill="#3B4262" d="M44.042 32.422l-.863-.86c-2.062-2.054-5.433-2.5-7.95-1.06l-5.376 3.059 1.12-24.502c0-2.054-1.678-3.726-3.743-3.726-.446 0-.875.079-1.273.222l.134-1.315c0-2.077-1.705-3.767-3.798-3.767-1.936 0-3.559 1.445-3.764 3.299l-3.45 20.75c-.045.282-.536.253-.588.075L10.416 7.962a3.658 3.658 0 00-3.502-2.629c-1.118 0-2.157.501-2.853 1.375a3.592 3.592 0 00-.69 3.08l.792 3.35a3.34 3.34 0 00-1.335.168 3.447 3.447 0 00-2.326 3.818L2.9 30.85c0 5.415.953 9.423 1.754 11.83a13.61 13.61 0 01.687 4.291c0 1.284-.179 2.562-.534 3.796l-1.86 6.482c-.104.366-.03.76.198 1.065.232.304.592.483.975.483h21.97a1.218 1.218 0 001.16-1.6c-.013-.031-1.033-3.169-.067-7.437 1.225-.99 5.514-4.462 7.054-5.862 2.546-2.315 9.521-9.468 9.817-9.772a1.21 1.21 0 00-.012-1.705z"/></svg>

After

Width:  |  Height:  |  Size: 836 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,234 @@
<!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 -->
<script src="public/deps/htmx.min.js"></script>
<link
href="../public/output.css"
th:href="'public/output.css'"
rel="stylesheet"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="../public/images/favicon-32x32.png"
th:href="'public/images/favicon-32x32.png'"
/>
<link
href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@600;700&display=swap"
rel="stylesheet"
/>
<title>Frontend Mentor | Rock, Paper, Scissors</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="flex flex-col justify-between items-center pt-8 w-screen h-screen bg-gradient-to-b from-radial-gradient-top to-radial-gradient-bottom"
>
<section
id="heading"
class="flex flex-row items-center w-10/12 rounded-md md:w-1/2 md:h-36 md:rounded-xl border-[3px] border-header-outline h-[100px]"
>
<h1
class="pl-4 text-xl font-bold leading-none text-white uppercase md:text-5xl grow md:leading-[2.2rem]"
>
<span class="block">rock</span> <span class="block">paper</span>
<span class="block">scissors</span>
</h1>
<h2
class="flex flex-col justify-around items-center py-2 mr-3 bg-white rounded-md md:mr-5 md:w-40 md:h-4/5 md:rounded-lg w-[80px] h-[70px] text-score-text rouned-md"
>
<span class="text-xs uppercase md:text-xl md:leading-none">Score</span>
<span id="the-score-number" class="text-4xl font-extrabold md:text-6xl">12</span>
<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>
</h2>
</section>
<section
id="controls"
class="bg-center bg-no-repeat bg-60% bg-triangle-pattern w-[375px] h-[375px] relative md:scale-150"
>
<!-- This control will be repeated 3 times, with different htmx requests -->
<div
th:each="choiceBadgeData : ${choiceBadges}"
id="paper-control"
th:id="${choiceBadgeData.c.name} + '-control'"
hx-get="/select/paper"
th:hx-get="'/select/' + ${choiceBadgeData.c.name}"
hx-target="#controls"
hx-swap="outerHTML"
>
<!-- This badge is fragment to be repeated in other pages as well -->
<div
th:fragment="choiceBadge (choiceBadgeData)"
th:id="${choiceBadgeData.c.name}"
id="paper"
class="top-[var(--top-offset)] left-[var(--left-offset)] w-[var(--diameter)] h-[var(--diameter)] bg-gradient-to-b rounded-full -translate-x-[var(--translation)] -translate-y-[var(--translation)] from-[var(--bg-bright)] to-[var(--bg-dark)]"
style="
--diameter: 8rem;
--bg-dark: hsl(230, 89%, 62%);
--bg-bright: hsl(230, 89%, 65%);
--top-offset: 6rem;
--left-offset: 6rem;
--translation: 50%;
position: absolute;
"
th:style="| --diameter: ${choiceBadgeData.s.diameter};
--bg-dark: ${choiceBadgeData.s.bgDark};
--bg-bright: ${choiceBadgeData.s.bgBright};
${choiceBadgeData.p.toStyle}; |"
>
<div
class="absolute top-1/2 left-1/2 w-3/4 h-3/4 bg-gradient-to-b from-gray-300 to-gray-100 rounded-full -translate-x-1/2 -translate-y-1/2"
></div>
<img
src="../public/images/icon-paper.svg"
th:src="${choiceBadgeData.c.iconPath}"
class="absolute top-1/2 left-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2"
/>
</div>
</div>
<!-- This controls is only for static preview -->
<div
th:remove="all"
id="scissors"
class="top-[var(--top-offset)] left-[var(--left-offset)] w-[var(--diameter)] h-[var(--diameter)] bg-gradient-to-b rounded-full -translate-x-[var(--translation)] -translate-y-[var(--translation)] from-[var(--bg-bright)] to-[var(--bg-dark)]"
style="
--diameter: 8rem;
--bg-dark: hsl(39, 89%, 49%);
--bg-bright: hsl(40, 84%, 53%);
--top-offset: 6rem;
--left-offset: 17rem;
--translation: 50%;
position: absolute;
"
>
<div
class="absolute top-1/2 left-1/2 w-3/4 h-3/4 bg-gradient-to-b from-gray-300 to-gray-100 rounded-full -translate-x-1/2 -translate-y-1/2"
></div>
<img
src="../public/images/icon-scissors.svg"
class="absolute top-1/2 left-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2"
/>
</div>
<!-- This controls is only for static preview -->
<div
th:remove="all"
id="rock"
class="top-[var(--top-offset)] left-[var(--left-offset)] w-[var(--diameter)] h-[var(--diameter)] bg-gradient-to-b rounded-full -translate-x-[var(--translation)] -translate-y-[var(--translation)] from-[var(--bg-bright)] to-[var(--bg-dark)]"
style="
--diameter: 8rem;
--bg-dark: hsl(349, 71%, 52%);
--bg-bright: hsl(349, 70%, 56%);
--top-offset: 15rem;
--left-offset: 11.5rem;
--translation: 50%;
position: absolute;
"
>
<div
class="absolute top-1/2 left-1/2 w-3/4 h-3/4 bg-gradient-to-b from-gray-300 to-gray-100 rounded-full -translate-x-1/2 -translate-y-1/2"
></div>
<img
src="../public/images/icon-rock.svg"
class="absolute top-1/2 left-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2"
/>
</div>
</section>
<!-- This is rules overlay modal window -->
<dialog
id="rules-dialog"
class="w-screen h-screen md:rounded-lg md:w-[400px] md:h-[400px]"
>
<div class="flex flex-col justify-between w-full h-full">
<h1
class="grid place-content-center h-1/4 text-3xl font-bold tracking-wide uppercase md:h-12"
>
Rules
</h1>
<div class="grid place-content-center grow">
<img
src="../public/images/image-rules.svg"
alt="Rules of the game: rock beats scissors, scissors beat paper, paper beats rock."
/>
</div>
<button
id="close-dialog-button"
class="grid place-content-center h-32 md:absolute md:top-5 md:w-12 md:h-12 md:end-5"
>
<img
src="../public/images/icon-close.svg"
alt="Close rules display"
/>
</button>
</div>
</dialog>
<div class="py-12">
<button
id="rules-button"
class="w-32 h-10 text-base text-2xl tracking-widest text-white uppercase rounded-lg border border-white"
>
Rules
</button>
<script type="text/javascript">
const dialog = document.getElementById("rules-dialog");
const openButton = document.getElementById("rules-button");
const closeButton = document.getElementById("close-dialog-button");
openButton.addEventListener("click", function () {
dialog.showModal();
});
closeButton.addEventListener("click", function () {
dialog.close();
});
</script>
</div>
</main>
<footer 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>.
</footer>
</body>
</html>

View File

@@ -0,0 +1,202 @@
<!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="../public/output.css"
th:href="'public/output.css'"
rel="stylesheet"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="../public/images/favicon-32x32.png"
th:href="'public/images/favicon-32x32.png'"
/>
<link
href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@600;700&display=swap"
rel="stylesheet"
/>
<title>Frontend Mentor | Rock, Paper, Scissors</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="flex flex-col justify-between items-center pt-8 w-screen h-screen bg-gradient-to-b from-radial-gradient-top to-radial-gradient-bottom"
>
<section
id="heading"
class="flex flex-row items-center w-10/12 rounded-md border-[3px] border-header-outline h-[100px]"
>
<h1
class="pl-4 text-xl font-bold leading-none text-white uppercase grow"
>
<span class="block">rock</span> <span class="block">paper</span>
<span class="block">scissors</span>
</h1>
<h2
class="flex flex-col justify-around items-center py-2 mr-3 bg-white rounded-md w-[80px] h-[70px] text-score-text rouned-md"
>
<span class="text-xs uppercase">Score</span
><span class="text-4xl font-extrabold">12</span>
</h2>
</section>
<div id="showdown-table" th:fragment="showdown-table (showdownState)" class="md:scale-150">
<section class="grid grid-cols-2 w-[375px] h-[300px]">
<div
id="players-choice"
class="flex relative flex-col items-center pt-14"
>
<!-- This will be imported fragment -->
<!-- <p th:text="${showdownState}">Hello</p> -->
<div
th:replace="index::choiceBadge (${showdownState.playersChoice})"
></div>
<!-- This is end of the game animated halo -->
<div
class="absolute mt-16 bg-white rounded-full opacity-5 -translate-y-1/2 w-[150px] h-[150px]"
th:classappend="${showdownState.isPlayerWin} ? 'quickly-appear' : 'invisible' "
></div>
<div
class="absolute mt-16 bg-white rounded-full opacity-5 -translate-y-1/2 w-[200px] h-[200px]"
th:classappend="${showdownState.isPlayerWin} ? 'quickly-appear' : 'invisible' "
></div>
<div
class="absolute mt-16 bg-white rounded-full opacity-5 -translate-y-1/2 w-[260px] h-[260px]"
th:classappend="${showdownState.isPlayerWin} ? 'quickly-appear' : 'invisible' "
></div>
<div
th:remove="all"
id="rock"
class="top-[var(--top-offset)] left-[var(--left-offset)] w-[var(--diameter)] h-[var(--diameter)] bg-gradient-to-b rounded-full -translate-x-[var(--translation)] -translate-y-[var(--translation)] from-[var(--bg-bright)] to-[var(--bg-dark)]"
style="
--diameter: 8rem;
--bg-dark: hsl(349, 71%, 52%);
--bg-bright: hsl(349, 70%, 56%);
--top-offset: 0rem;
--left-offset: 0rem;
--translation: 0%;
position: relative;
"
>
<div
class="absolute top-1/2 left-1/2 w-3/4 h-3/4 bg-gradient-to-b from-gray-300 to-gray-100 rounded-full -translate-x-1/2 -translate-y-1/2"
></div>
<img
src="../public/images/icon-rock.svg"
class="absolute top-1/2 left-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2"
/>
</div>
<p
class="absolute top-2/3 tracking-widest text-center text-white uppercase text-md"
>
You picked
</p>
</div>
<div
id="house-choice"
class="flex relative flex-col items-center pt-14"
>
<!-- Here will be imported fragment -->
<!-- conditionally either house choice or request for house choice -->
<!-- <div th:text="${showdownState.houseChoice.nonEmpty} ? 'nonEmpty' : 'empty'">...</div> -->
<div
th:replace="${showdownState.houseChoice.nonEmpty} ? ~{index::choiceBadge (${showdownState.houseChoice.get})} : ~{::house-choice-placeholder}"
class="w-0 h-0"
th:class=""
>
...
</div>
<!-- This is end of the game animated halo for house choice -->
<div
class="absolute mt-16 bg-white rounded-full opacity-5 -translate-y-1/2 w-[150px] h-[150px]"
th:classappend="${showdownState.isHouseWin} ? 'quickly-appear' : 'invisible' "
></div>
<div
class="absolute mt-16 bg-white rounded-full opacity-5 -translate-y-1/2 w-[200px] h-[200px]"
th:classappend="${showdownState.isHouseWin} ? 'quickly-appear' : 'invisible' "
></div>
<div
class="absolute mt-16 bg-white rounded-full opacity-5 -translate-y-1/2 w-[260px] h-[260px]"
th:classappend="${showdownState.isHouseWin} ? 'quickly-appear' : 'invisible' "
></div>
<!-- This will be shown before the house made the choice, this will trigger timed request for house choice -->
<div class="w-full h-full" th:remove="all">
<div
class="flex relative flex-col items-center h-full"
th:fragment="house-choice-placeholder"
th:hx-get="'/house-choice/' + ${showdownState.playersChoice.c.name}"
hx-get="/house-choice/paper"
hx-target="#showdown-table"
hx-trigger="load delay:3s"
hx-swap="outerHTML"
>
<div
class="rounded-full mt-[1rem] bg-radial-gradient-top h-[100px] w-[100px]"
></div>
<div
class="absolute bg-white rounded-full opacity-20 w-[100px] h-[100px] mt-[1rem] pulsing-animation"
></div>
</div>
</div>
<p
class="absolute top-2/3 tracking-widest text-center text-white uppercase text-md"
>
The house picked
</p>
</div>
</section>
<!-- FRAGMENT : showdown result -->
<section
id="message"
class="flex flex-col items-center text-white slowly-appear"
th:classappend="${showdownState.gameResult.nonEmpty} ? '' : 'invisible'"
>
<p
class="text-6xl font-bold text-center uppercase"
th:text="${showdownState.gameResult.nonEmpty} ? 'You ' + ${showdownState.gameResult.get} : 'awesome'"
>
You lose
</p>
<a
class="grid place-content-center mt-6 w-9/12 h-12 tracking-widest uppercase bg-white rounded-xl text-radial-gradient-bottom"
href="/"
>
Play again
</a>
</section>
</div>
<div class="py-12">
<button
class="w-32 h-10 text-base text-2xl tracking-widest text-white uppercase rounded-lg border border-white"
>
Rules
</button>
</div>
</main>
<footer class="fixed inset-x-0 bottom-0 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,132 @@
package rockpaperscissors
import mainargs.{main, arg, ParserForMethods}
import cask.main.Routes
import org.thymeleaf.context.Context
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
import org.thymeleaf.TemplateEngine
import org.thymeleaf.Thymeleaf
import scala.jdk.CollectionConverters._
import javax.swing.text.Position
import rockpaperscissors.Models.Positioning
import scala.util.Random
import rockpaperscissors.Models.ShowdownState
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 = {
println(s"got args : $args")
ParserForMethods(this).runOrExit(args)
}
case class AppRoutes(pathPrefix: String = "")(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(s"$pathPrefix/")
def index(req: cask.Request) = {
val context = new Context()
val choices = Models.choiceSelectionItems.asJava
context.setVariable(
"choiceBadges",
choices
)
val result = templateEngine.process("index", context)
cask.Response(
result,
headers = Seq("Content-Type" -> "text/html;charset=UTF-8")
)
}
@cask.get(s"$pathPrefix/select/:choice")
def acceptPlayerVote(choice: String) = {
val context = new Context()
val badge = Models.choiceSelectionItems.find(_.c.name == choice)
val response = badge match {
case Some(playersChoiceBadge) =>
val badge = playersChoiceBadge.copy()
badge.p = Positioning.Relative
val showdownState = ShowdownState(badge, None, false)
context.setVariable("showdownState", showdownState)
val result = templateEngine.process(
"showdown",
Set("showdown-table").asJava,
context
)
cask.Response(
result,
headers = Seq("Content-Type" -> "text/html;charset=UTF-8")
)
case None =>
cask.Response(s"Unknown choice: '${choice}'", 400)
}
response
}
@cask.get(s"$pathPrefix/house-choice/:playersChoice")
def requestHouseChoice(playersChoice: String) = {
val context = new Context()
val badge = Models.choiceSelectionItems.find(_.c.name == playersChoice)
val response = badge match {
case Some(playersChoiceBadge) =>
val badge = playersChoiceBadge.copy()
badge.p =
Positioning.Relative // this probably should be set in enclosing html tag
val houseChoice =
Models.choiceSelectionItems(Random.nextInt(3)).copy()
houseChoice.p = Positioning.Relative
println(s"getting house choice $houseChoice")
val showdownState = ShowdownState(badge, Some(houseChoice), false)
context.setVariable("showdownState", showdownState)
val result = templateEngine.process(
"showdown",
Set("showdown-table").asJava,
context
)
cask.Response(
result,
headers = Seq(
"Content-Type" -> "text/html;charset=UTF-8",
"HX-Trigger-After-Settle" -> s"""{"updateScore": ${showdownState.scoreChange}}"""
)
)
case None =>
cask.Response(s"Unknown choice: '${playersChoice}'", 400)
}
response
}
@cask.staticResources(s"$pathPrefix/public")
def publicFiles(req: cask.Request) = {
"public"
}
initialize()
}
}

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