-
Notifications
You must be signed in to change notification settings - Fork 2
Task solution from fierce-rabbit #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.29 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](postcss/postcss@8.4.29...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com>
Bump postcss from 8.4.29 to 8.4.31
|
Is this ready for review or are there going to be more changes? |
| npm ci | ||
| npm start | ||
| ``` | ||
| test stuff |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is here from testing out the scrive-cat deployment and accidentally commit to master. Please ignore this line.
|
Ready for review I would say. Of course, there is still space for improvement but it's always. :) |
src/Main.elm
Outdated
| Value.addError "Name is required" nameValue | ||
|
|
||
| else if List.any (.name >> Editable.valueFromEditable >> Value.fromValue >> (==) nameString) model.tags then | ||
| Value.addError "Duplicate name" nameValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storing your validation error messages in the Model is a bad idea, they are redundant information.
Case in point:
- Enter a duplicate Tag Name (an error hint will correctly display)
- Delete the duplicate tag
- The "duplicate" error hint does not disappear
This approach makes your update function more complicated than it needs to be and more prone to errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion! Just to clarify, in this code globalError only stores JSON decoding errors.
Validation errors for inputs (like duplicate tag names) are already handled directly in their Value, so they dynamically update without storing redundant information in the Model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion! Just to clarify, in this code globalError only stores JSON decoding errors.
Validation errors for inputs (like duplicate tag names) are already handled directly in their Value, so they dynamically update without storing redundant information in the Model.
But for the scenario that you mentioned, I added validation also for ClickedRemoveTag message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both globalError and the error stored inside a Value are redundant, because they can be derived from information you have already.
In the case of globalError, its value can always be derived from the JSON.
We could say that we are pretending we got that data from a remote server, but in that case I think it would be better to store the Result produced by Json.decodeString.
In the case of Value, we can derive the error simply from the Model.
We just need the input value and the list of the other tags to know to know whether it is duplicate or not.
Storing the error message inside Value is redundant and results in a (minor, in this case) bug.
src/Main.elm
Outdated
| emptyNewTagForm : Tag | ||
| emptyNewTagForm = | ||
| { name = Value.toValue "" |> Editable.valueToEditable | ||
| , value = Value.toValue "" |> Editable.valueToEditable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reads a bit weird. Value.toValue sounds like I take a Value and transform it into a Value.
Perhaps Value.fromString?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function works for any type of value, not just String. I could rename it to something like Value.validValue to make that clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't Value.valid be enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function works for any type of value, not just String. I could rename it to something like Value.validValue to make that clearer
src/Main.elm
Outdated
| updatedTags = | ||
| List.map | ||
| (\tag -> | ||
| if (Editable.valueFromEditable tag.name |> Value.fromValue) == nameString then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Editable.toValue would read better.
src/Main.elm
Outdated
| ---- VIEW ---- | ||
| ClickedRemoveTag nameToRemove -> | ||
| ( { model | ||
| | tags = List.filter (.name >> Editable.valueFromEditable >> Value.fromValue >> (/=) nameToRemove) model.tags |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the systematic approach, but OTOH we need to keep unwrapping (twice!) and wrapping these Strings and I'm not sure there's a clear benefit, it feels like it may be perhaps a bit overengineered?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand your constrain and you're right - in such a simple project it's not necessary.
I mainly used this approach to demonstrate that I’m comfortable working with more advanced data structures and patterns, even if they aren’t strictly required here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. =)
| subheader : String -> Html msg | ||
| subheader text = | ||
| Html.span [ Attrs.class "p-2 text-2xl font-extrabold text-slate-800" ] | ||
| Html.span [ Attrs.class "p-2 text-2xl font-extrabold text-[#1A1A1A]" ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you decide to use color literals instead than constants?
kraklin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the solution to our hiring task. I quite like it. Splitting the UI parts away to separate modules in Shared is a nice touch for sure.
UI is clear and navigable by keyboard with usage of aria tags and/or titles for image icons - nice :) I'd add the possibility to enter the tag from the form by hitting Enter as I was doing that repeatedly when trying to navigate only by keyboard.
Code reads mostly good, there is one or two places that feels too unnecessarily nested to me.
I left some suggestions and questions in the comments. Can you expand on them there? 🙏
src/Shared/Tooltip.elm
Outdated
| view : String -> String -> Html msg | ||
| view id tooltipText = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you choose the view to have String -> String type? Do you know why and under what circumstances it is a problem and how to mitigate it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, String types can be accidentally swapped, so it's better to use record in cases like this. I will fix it.
| type IconType | ||
| = Add | ||
| | Edit | ||
| | Checkmark | ||
| | Refresh | ||
| | Remove |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you go with type variant and not exposing the icons as separate functions? What are the benefits of this approach? What are the drawbacks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose to use a type variant IconType because it's simple and consistent. It allows calling Icon.view Icon.Add uniformly and easily mapping over lists of icons.
The main drawback is that every render requires a case expression, and it’s slightly less flexible if we need customization.
Using separate functions for each icon would avoid the case but make it harder to dynamically work with collections of icons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my solution I used inspiration from elm components pattern (https://elm.land/concepts/components.html) what is approach that makes sense to me :)
src/Shared/Value.elm
Outdated
| toValue : a -> Value a | ||
| toValue = | ||
| Valid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You expect the value to be valid straight away during the conversion to Value. What if we'd like to validate this value based on some validators? Would you do some changes to the Value module in such case or would you keep it as it is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose to have toValue create a Valid value directly because my goal was mainly to demonstrate the Value module.
I didn’t want to go into unnecessary details of full validation logic for this task, but the module could be extended with validators if needed.
src/Shared/Value.elm
Outdated
| isValid : Value a -> Bool | ||
| isValid value = | ||
| case value of | ||
| Invalid _ _ -> | ||
| False | ||
|
|
||
| Valid _ -> | ||
| True | ||
|
|
||
|
|
||
| isInvalid : Value a -> Bool | ||
| isInvalid value = | ||
| case value of | ||
| Invalid _ _ -> | ||
| True | ||
|
|
||
| Valid _ -> | ||
| False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO this could be simplified:
isInvalid =
not << isValidThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I overlooked this one - thanks for pointing it out :)
src/Shared/Editable.elm
Outdated
| updateIfEditable : Editable a -> a -> Editable a | ||
| updateIfEditable editable newValue = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd IMHO switch the order of parameters for better composability to a -> Editable a -> Editable a. Why did you use this order here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, switching the parameters would make it easier to use this function with mapping, and it also aligns better with Elm conventions.
| ( tooltipId, tooltipAttributes ) = | ||
| let | ||
| tooltipId_ = | ||
| "tooltip-" ++ Maybe.withDefault "button" label |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Under which circumstances this can backfire? And how would you mitigate the problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which exact part of the code do you mean? I’ve committed a few times in the meantime, so I’m not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I mean the fact that you are creating element ID from label. What might be the problem here?
src/Main.elm
Outdated
| Value.addError "Name is required" nameValue | ||
|
|
||
| else if List.any (.name >> Editable.valueFromEditable >> Value.fromValue >> (==) nameString) model.tags then | ||
| Value.addError "Duplicate name" nameValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nesting of let .. in inside let .. in is a codesmell to me at it might be quite challenging to follow which value is from where and it adds complexity to overall code readability. Would it be possible to refactor it out? How?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overall lots is going on inside upadte function. Do you have an idea how to make it more readable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that this can make the code harder to read. One solution could be to use a single let...in block, or better, to extract parts of the logic into separate helper functions. In a real project, I would likely create a dedicated module for validators to keep the code clean and modular.
|
|
||
| type alias Model = | ||
| {} | ||
| { tags : List Tag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why you choose to use List here instead of e.g. Dict? Can you elaborate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose List because the number of tags is expected to be small and operations like adding, removing, or iterating over them are straightforward.
Using a Dict could be beneficial if we needed fast lookups by key or expected a very large collection, but for this task a List keeps the code simpler and more readable.
src/Main.elm
Outdated
| ] | ||
| ++ (if Editable.isEditable editable then | ||
| [] | ||
|
|
||
| else | ||
| [ Attrs.attribute "readonly" "true" ] | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is Html.Attributes.Extra.attributeIf in Html.Extra package that is quite helpful with these conditional attributes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know this but I love it, thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very useful and elegant, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love it, thank you :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love it, thanks :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this, thanks for sharing :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this, thank you :)
src/Main.elm
Outdated
| , if Editable.isEditable editable then | ||
| Html.text "" | ||
|
|
||
| else | ||
| Html.div | ||
| [ Attrs.class "pointer-events-none absolute inset-0 flex items-center px-3 text-gray-700" | ||
| ] | ||
| [ Html.text valueString ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is Html.Extra.viewIf in the Html.Extra package that shines in situations like these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very useful and elegant, thanks!
src/Main.elm
Outdated
| let | ||
| newTagFormView = | ||
| Html.div [ Attrs.class "mb-6 grid grid-cols-1 md:grid-cols-[2fr_2fr_auto] gap-3 items-center w-full max-w-full" ] | ||
| [ inputField "Tag Name *" model.newTagForm.name UpdateNewName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no explanation of what "*" means.
I would but a more explicit "(required)".
| ] | ||
| >> List.singleton | ||
| ) | ||
| >> Maybe.withDefault [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like a case..of here would be more readable.
src/Main.elm
Outdated
|
|
||
| else | ||
| [ Attrs.attribute "readonly" "true" ] | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure of the accessibility implications of creating an <input> field just to set it to readonly.
src/Shared/Button.elm
Outdated
| , Attrs.attribute "aria-label" (Maybe.withDefault "Button" label) | ||
| ] | ||
| ++ tooltipAttributes | ||
| ++ [ Events.onClick onClickMsg ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this added by itself rather than together with the first List of attributes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I’ve already pushed a few changes, so I’m not sure which part of the code this comment is about.
| , SvgAttrs.d "M6 18L18 6M6 6l12 12" | ||
| ] | ||
| [] | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the advantage of having all the icons in a single function rather than each their own constant?
(ie, Shared.Icon.add, Shared.Icon.edit... and so on)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Nevermind, I see Tomas asked the same question.)
|
The The UI feels good, is clear and easy to interact with. There is a bit of overengineering, but I'll write it off as the candidate having fun with Elm, which is a plus. =) |
|
Thanks! I see what you mean about the Shared modules. I agree it adds some complexity to the user code, but I wanted to show working with reusable components. I’m glad that you like the visual part! |
|
No worries. |
|
No worries at all — I didn’t take it as overly critical. |
3c8379a to
006a1e7
Compare
This PR contains solution from the candidate fierce-rabbit