Skip to content

Commit

Permalink
Merge pull request #11 from JulienR1/features/better-icon-selector
Browse files Browse the repository at this point in the history
Better icon selector
  • Loading branch information
JulienR1 authored Jan 2, 2024
2 parents 661b32b + e5705e3 commit 0763bd4
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 75 deletions.
4 changes: 4 additions & 0 deletions server/internal/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ func RegisterRoutes(app *fiber.App, db *repoutils.Database) {
tokenService := services.MakeTokenService()
cookieService := services.MakeCookieService()
fileService := services.MakeFileService(&fileRepository)
iconService := services.MakeIconService(&iconRepository)
authService := services.MakeAuthService(&tokenService, &userRepository)
userService := services.MakeUserService(&userRepository, &dashboardRepository, db)
categoryService := services.MakeCategoryService(&categoryRepository, &iconRepository)
dashboardService := services.MakeDashboardService(&dashboardRepository, &categoryService, &userService)
transactionService := services.MakeTransactionService(&transactionRepository, &userService, &fileService, &categoryService)

iconHandler := MakeIconHandler(&iconService)
userHandler := MakeUserHandler(validator, &userService)
categoryHandler := MakeCategoryHandler(validator, &categoryService, &dashboardService)
authHandler := MakeAuthHandler(validator, &authService, &tokenService, &cookieService)
Expand All @@ -54,6 +56,8 @@ func RegisterRoutes(app *fiber.App, db *repoutils.Database) {
users := api.Group("/users").Use(authMiddleware)
users.Get("/:userId", userHandler.GetUser)

api.Get("/icons", iconHandler.GetIcons)

api.Use(authMiddleware).Get("/dashboards", dashboardHandler.GetAllDashboardsForUser)
api.Use(authMiddleware).Get("/dashboards/:dashboardId", dashboardHandler.GetDashboardForUser)

Expand Down
24 changes: 24 additions & 0 deletions server/internal/handlers/icon-handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package handlers

import (
"JulienR1/moneymanager2/server/internal/services"
"net/http"

"github.com/gofiber/fiber/v2"
)

type IconHandler struct {
service *services.IconService
}

func MakeIconHandler(s *services.IconService) IconHandler {
return IconHandler{service: s}
}

func (handler *IconHandler) GetIcons(c *fiber.Ctx) error {
icons, err := handler.service.GetIcons()
if err != nil {
return c.SendStatus(http.StatusInternalServerError)
}
return c.Status(http.StatusOK).JSON(icons)
}
26 changes: 23 additions & 3 deletions server/internal/repositories/icon-repository.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package repositories

import repoutils "JulienR1/moneymanager2/server/internal/pkg/repo-utils"
import (
repoutils "JulienR1/moneymanager2/server/internal/pkg/repo-utils"
)

type IconRepository struct {
db *repoutils.Database
Expand All @@ -15,13 +17,31 @@ func MakeIconRepository(db *repoutils.Database) IconRepository {
return IconRepository{db: db}
}

func (repo *IconRepository) FindByName(name string) (*IconRecord, error) {
func (repository *IconRepository) FindByName(name string) (*IconRecord, error) {
query := "SELECT id, label FROM icons WHERE label = $1"

var record IconRecord
if err := repo.db.Connection.QueryRow(query, name).Scan(&record.Id, &record.Label); err != nil {
if err := repository.db.Connection.QueryRow(query, name).Scan(&record.Id, &record.Label); err != nil {
return nil, err
}

return &record, nil
}

func (repository *IconRepository) FindAvailableIcons() ([]string, error) {
query := "SELECT label FROM icons"

rows, err := repository.db.Connection.Query(query)
if err != nil {
return []string{}, err
}

icons := []string{}
for rows.Next() {
var icon string
rows.Scan(&icon)
icons = append(icons, icon)
}

return icons, nil
}
22 changes: 22 additions & 0 deletions server/internal/services/icon-service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package services

import (
"JulienR1/moneymanager2/server/internal/repositories"
"errors"
)

type IconService struct {
repository *repositories.IconRepository
}

func MakeIconService(r *repositories.IconRepository) IconService {
return IconService{repository: r}
}

func (service *IconService) GetIcons() ([]string, error) {
icons, err := service.repository.FindAvailableIcons()
if err != nil {
return []string{}, errors.New("could not find icons")
}
return icons, nil
}
44 changes: 14 additions & 30 deletions web/src/modules/categories/new-category-form.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Button, ColorInput, Form, Input } from "@modules/form";
import { Button, ColorInput, Form, Input, Select } from "@modules/form";
import { A, useNavigate } from "@solidjs/router";
import { Component } from "solid-js";
import { Skeleton } from "@ui";
import { Component, For, Suspense, createResource } from "solid-js";
import { CategorySchema } from "./schemas";
import { createCategory } from "./service";
import { createCategory, findAvailableIcons } from "./service";

type NewCategoryFormProps = {
closeLocation: string;
Expand All @@ -12,44 +13,27 @@ type NewCategoryFormProps = {

export const NewCategoryForm: Component<NewCategoryFormProps> = (props) => {
const navigate = useNavigate();
const [icons] = createResource(findAvailableIcons);

function closeForm() {
navigate(props.closeLocation);
}

return (
<Form
onSubmit={(d) =>
createCategory(d, props.dashboardId, props.refreshDashboard, closeForm)
}
onSubmit={(d) => createCategory(d, props.dashboardId, props.refreshDashboard, closeForm)}
schema={CategorySchema}
>
<Input
id="label"
name="label"
label="Nom"
placeholder="Saisir un nom"
leftIcon={{ name: "label" }}
/>
<ColorInput
id="color"
name="color"
label="Couleur"
placeholder="Saisir une couleur (HEX)"
/>
<Input
id="icon"
name="icon"
label="Icône"
placeholder="Saisir un icône"
leftIcon={{ name: "image" }}
/>
<Input id="label" name="label" label="Nom" placeholder="Saisir un nom" leftIcon={{ name: "label" }} />
<ColorInput id="color" name="color" label="Couleur" placeholder="Saisir une couleur (HEX)" />
<Suspense fallback={<Skeleton type="line" />}>
<Select id="icon" name="icon" label="Icône" placeholder="Saisir un icône" leftIcon={{ name: "image" }}>
<For each={icons()}>{(icon) => <option value={icon}>{icon}</option>}</For>
</Select>
</Suspense>

<div class="mt-2 flex items-center justify-between">
<A
href={props.closeLocation}
class="ml-2 block w-fit text-sm text-red-500 underline"
>
<A href={props.closeLocation} class="ml-2 block w-fit text-sm text-red-500 underline">
Annuler
</A>
<Button type="submit" icon={{ name: "check" }}>
Expand Down
11 changes: 11 additions & 0 deletions web/src/modules/categories/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { request } from "@modules/fetch";
import { cookToast } from "@modules/toasts";
import { z } from "zod";
import { CategorySchema, NewCategorySchema } from "./schemas";

export async function createCategory(
Expand All @@ -26,3 +27,13 @@ export async function createCategory(
refreshDashboard();
onSuccess();
}

export async function findAvailableIcons(): Promise<string[]> {
const result = await request(`/icons`).get(z.array(z.string()));
if (!result.success) {
cookToast("Erreur", { description: "Impossible de charger les icônes" }).burnt();
return [];
}

return result.data;
}
109 changes: 67 additions & 42 deletions web/src/modules/form/components/input.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,92 @@
import { Icon, IconProps } from "@ui/icon";
import { Component, JSX, Show, createUniqueId } from "solid-js";
import { Component, JSX, ParentProps, Show, children, createUniqueId } from "solid-js";
import { FieldError } from "./field-error";
import { useForm } from "./form";

export type InputProps = Omit<
JSX.InputHTMLAttributes<HTMLInputElement>,
"oninput" | "onInput"
> & {
export type InputProps = Omit<JSX.InputHTMLAttributes<HTMLInputElement>, "oninput" | "onInput"> & {
onInput?: JSX.InputEventHandler<HTMLInputElement, InputEvent>;
} & WrapperProps;

export type SelectProps = Omit<JSX.InputEventHandlerUnion<HTMLSelectElement, InputEvent>, "oninput" | "onInput"> & {
onInput?: JSX.InputEventHandler<HTMLSelectElement, InputEvent>;
placeholder?: string;
} & WrapperProps;

type WrapperProps = ParentProps<{
id: string;
label: string;
name: string;
leftIcon?: IconProps;
rightIcon?: IconProps;
onInput?: JSX.InputEventHandler<HTMLInputElement, InputEvent>;
};

export const Input: Component<InputProps> = (props) => {
const { validateForm } = useForm();

const handleInput: JSX.InputEventHandler<HTMLInputElement, InputEvent> = (
e,
) => {
validateForm();
props.onInput?.(e);
};

const id = props.id + createUniqueId();
}>;

const Wrapper: Component<WrapperProps> = (props) => {
return (
<div class="py-2">
<label
for={id}
class="flex items-center rounded-lg bg-white shadow shadow-gray-400 focus-within:shadow-lg"
>
<label for={props.id} class="flex items-center rounded-lg bg-white shadow shadow-gray-400 focus-within:shadow-lg">
<Show when={props.leftIcon}>
<Icon
{...props.leftIcon!}
class={"ml-2 " + props.leftIcon?.class ?? ""}
size="lg"
mdSize="xl"
/>
<Icon {...props.leftIcon!} class={"ml-2 " + props.leftIcon?.class ?? ""} size="lg" mdSize="xl" />
</Show>

<div class="w-full">
<p class="translate-y-1 px-2 text-xs font-semibold text-gray-500">
{props.label}
</p>
<input
{...props}
id={id}
onInput={handleInput}
class="w-full rounded-lg p-1 pt-0 text-xs font-semibold text-black focus-visible:outline-none md:p-2 md:text-sm"
/>
<p class="translate-y-1 px-2 text-xs font-semibold text-gray-500">{props.label}</p>
{props.children}
</div>

<Show when={props.rightIcon}>
<Icon
{...props.rightIcon!}
class={"mr-2 " + props.rightIcon?.class ?? ""}
/>
<Icon {...props.rightIcon!} class={"mr-2 " + props.rightIcon?.class ?? ""} />
</Show>
</label>

<FieldError name={props.name} />
</div>
);
};

export const Input: Component<InputProps> = (props) => {
const { validateForm } = useForm();
const id = props.id + createUniqueId();

const handleInput: JSX.InputEventHandler<HTMLInputElement, InputEvent> = (e) => {
validateForm();
props.onInput?.(e);
};

return (
<Wrapper {...props} id={id}>
<input
{...props}
id={id}
onInput={handleInput}
class="w-full rounded-lg p-1 pt-0 text-xs font-semibold text-black focus-visible:outline-none md:p-2 md:text-sm"
/>
</Wrapper>
);
};

export const Select: Component<SelectProps> = (props) => {
const { validateForm } = useForm();
const id = props.id + createUniqueId();
const c = children(() => props.children);

const handleInput: JSX.InputEventHandler<HTMLSelectElement, InputEvent> = (e) => {
validateForm();
props.onInput?.(e);
};

return (
<Wrapper {...props} id={id}>
<select
{...props}
id={id}
onInput={handleInput}
class="w-full rounded-lg p-1 pt-0 text-xs font-semibold text-black focus-visible:outline-none md:p-2 md:text-sm"
>
<Show when={props.placeholder && props.placeholder.length > 0}>
<option>{props.placeholder}</option>
</Show>
{c()}
</select>
</Wrapper>
);
};

0 comments on commit 0763bd4

Please sign in to comment.