refactor icons. Somehow button colors are no longer working...

This commit is contained in:
Anika Raemer 2025-10-12 17:17:21 +02:00
parent a224397079
commit 3f075d509b
16 changed files with 152 additions and 110 deletions

View file

@ -7,7 +7,7 @@
/* background */ /* background */
.app-bg { .app-bg {
@apply flex items-center w-screen justify-center min-h-screen bg-gray-50 @apply flex items-center w-screen justify-center min-h-screen bg-gray-50;
} }
/* headings */ /* headings */
@ -23,49 +23,55 @@
@apply font-semibold mb-2 mt-4; @apply font-semibold mb-2 mt-4;
} }
/* icons */
.default-icon {
@apply text-gray-400 hover:text-gray-500;
}
/* labels */ /* labels */
.label { .label {
@apply text-gray-600 @apply text-gray-600;
} }
/* errors */ /* errors */
.error-text { .error-text {
@apply text-sm text-red-600 @apply text-sm text-red-600;
} }
/* buttons */ /* buttons */
.basic-button{ .basic-button{
@apply px-4 py-2 shadow-md rounded-lg whitespace-nowrap @apply px-4 py-2 shadow-md rounded-lg whitespace-nowrap;
} }
.default-button-bg { .default-button-bg {
@apply bg-gray-300 hover:bg-gray-400 @apply bg-gray-300 hover:bg-gray-400;
} }
.default-button-text{ .default-button-text{
@apply text-gray-600 @apply text-gray-600;
} }
.primary-button-bg { .primary-button-bg {
@apply bg-blue-300 hover:bg-blue-400 @apply bg-blue-300 hover:bg-blue-400;
} }
.primary-button-text { .primary-button-text {
@apply text-gray-600 @apply text-gray-600;
} }
.dark-button-bg{ .dark-button-bg{
@apply bg-gray-600 hover:bg-gray-800 @apply bg-gray-600 hover:bg-gray-800;
} }
.dark-button-text{ .dark-button-text{
@apply text-white @apply text-white;
} }
.transparent-button-bg { .transparent-button-bg {
@apply bg-transparent hover:bg-transparent @apply bg-transparent hover:bg-transparent;
} }
.transparent-button-text { .transparent-button-text {
@apply text-gray-600 @apply text-gray-600;
} }
/* input fields like input and textarea */ /* input fields like input and textarea */
.input-field { .input-field {
@apply p-2 w-full border rounded-md placeholder-gray-400 border-gray-600 hover:border-blue-600 transition-colors text-gray-600 focus:outline-none focus:border-blue-800; @apply p-2 w-full border rounded-md placeholder-gray-400 border-gray-600 hover:border-blue-800 transition-colors text-gray-600 focus:outline-none focus:border-blue-900;
} }
/* groups */ /* groups */
@ -87,7 +93,7 @@
} }
.enumeration-indicator{ .enumeration-indicator{
@apply flex-shrink-0 w-7 h-7 rounded-full bg-blue-300 text-white flex items-center justify-center shadow-sm @apply flex-shrink-0 w-7 h-7 rounded-full bg-blue-300 text-white flex items-center justify-center shadow-sm;
} }
} }

View file

@ -1,11 +1,12 @@
import { useState } from "react"; import { useState } from "react";
import Button, { ButtonType } from "./basics/Button"; import Button from "./basics/Button";
import type { LoginRequestDto } from "../api/dtos/LoginRequestDto"; import type { LoginRequestDto } from "../api/dtos/LoginRequestDto";
import type { LoginResponseDto } from "../api/dtos/LoginResponseDto"; import type { LoginResponseDto } from "../api/dtos/LoginResponseDto";
import { login } from "../api/points/AuthPoint"; import { login } from "../api/points/AuthPoint";
import { getRecipeListUrl } from "../routes"; import { getRecipeListUrl } from "../routes";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import PasswordField from "./basics/PasswordField"; import PasswordField from "./basics/PasswordField";
import { ButtonType } from "./basics/BasicButtonDefinitions";
export default function LoginPage() { export default function LoginPage() {
const [userName, setUserName] = useState<string>(""); const [userName, setUserName] = useState<string>("");
@ -14,6 +15,9 @@ export default function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
/**
* Login
*/
const executeLogin = async () => { const executeLogin = async () => {
const dto: LoginRequestDto = { const dto: LoginRequestDto = {
userName, userName,
@ -26,6 +30,7 @@ export default function LoginPage() {
localStorage.setItem("session", JSON.stringify(loginResponse)); localStorage.setItem("session", JSON.stringify(loginResponse));
console.log("Successfully logged in as " + loginResponse.userData?.userName); console.log("Successfully logged in as " + loginResponse.userData?.userName);
setErrorMessage(null); setErrorMessage(null);
// navigate to recipe list after successful login
navigate(getRecipeListUrl()); navigate(getRecipeListUrl());
} catch (err: any) { } catch (err: any) {
console.error("Login failed:", err); console.error("Login failed:", err);

View file

@ -0,0 +1,35 @@
import type { LucideIcon } from "lucide-react";
export type BasicButtonProps = {
/** Optional Lucide icon (e.g. Plus, X, Check) */
icon?: LucideIcon;
text?: string;
buttonType?: ButtonType;
/** Optional additional style */
className?: string;
}
/**
* Define button types here.
* Export as enum like class.
*/
export const ButtonType = {
DarkButton: {
textColor: "text-dark-button-text",
backgroundColor: "bg-dark-button-bg",
},
PrimaryButton: {
textColor: "text-primary-button-text",
backgroundColor: "bg-primary-button-bg",
},
DefaultButton: {
textColor: "text-default-button-text",
backgroundColor: "bg-default-button-bg",
},
TransparentButton: {
textColor: "text-transparent-button-text",
backgroundColor: "bg-transparent-button-bg",
},
} as const;
export type ButtonType = typeof ButtonType[keyof typeof ButtonType];

View file

@ -1,52 +1,32 @@
import SvgIcon, { Icon } from "./SvgIcon" import { defaultIconSize } from "./SvgIcon";
import { ButtonType, type BasicButtonProps } from "./BasicButtonDefinitions";
type ButtonProps = { type ButtonProps = BasicButtonProps & {
onClick: () => void, onClick: () => void;
icon?: Icon, };
text?: string,
buttonType?: ButtonType export default function Button({
className?: string onClick,
icon: Icon,
text,
buttonType = ButtonType.DefaultButton,
className = "",
...props
}: ButtonProps) {
return (
<button
className={`basic-button ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`}
onClick={onClick}
{...props}
>
<div className="flex items-center gap-2">
{Icon && (
<Icon
size={defaultIconSize}
/>
)}
{text}
</div>
</button>
);
} }
export const ButtonType = {
DarkButton: {
textColor: "dark-button-text",
backgroundColor: "dark-button-bg"
},
PrimaryButton: {
textColor: "primary-button-text",
backgroundColor: "primary-button-bg"
},
DefaultButton: {
textColor: "default-button-text",
backgroundColor: "default-button-bg"
},
TransparentButton: {
textColor: "transparent-button-text",
backgroundColor: "transparent-button-bg"
}
} as const;
export type ButtonType = typeof ButtonType[keyof typeof ButtonType];
export default function Button(
{
onClick: onClick,
icon, text,
buttonType = ButtonType.DefaultButton,
className = ""
}: ButtonProps) {
return (
<button
type="button"
className={`basic-button ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`}
onClick={onClick}
>
<div className="flex items-center gap-2">
{/* Render icon only if defined */}
{icon && <SvgIcon icon={icon} color={buttonType.textColor} />}
{text}
</div>
</button>
)
}

View file

@ -1,10 +1,10 @@
import { Link, type LinkProps } from "react-router-dom" import { Link, type LinkProps } from "react-router-dom"
import { ButtonType } from "./Button" import { defaultIconSize } from "./SvgIcon"
import { ButtonType, type BasicButtonProps } from "./BasicButtonDefinitions"
type ButtonLinkProps = LinkProps & { type ButtonLinkProps = LinkProps & BasicButtonProps
text: string & {
buttonType?: ButtonType to: string
className?: string
} }
/** /**
@ -13,6 +13,7 @@ type ButtonLinkProps = LinkProps & {
export default function ButtonLink({ export default function ButtonLink({
to, to,
text, text,
icon: Icon,
buttonType = ButtonType.DefaultButton, buttonType = ButtonType.DefaultButton,
className = "", className = "",
...props ...props
@ -23,7 +24,14 @@ export default function ButtonLink({
className={`basic-button ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`} className={`basic-button ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`}
{...props} {...props}
> >
{text} <div className="flex items-center gap-2">
{Icon && (
<Icon
size={defaultIconSize}
/>
)}
{text}
</div>
</Link> </Link>
) )
} }

View file

@ -1,5 +1,6 @@
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { defaultIconSize } from "./SvgIcon";
type PasswordFieldProps = { type PasswordFieldProps = {
onPasswordChanged: (password : string) => void onPasswordChanged: (password : string) => void
@ -12,7 +13,6 @@ type PasswordFieldProps = {
export default function PasswordField({onPasswordChanged, onKeyDown} : PasswordFieldProps){ export default function PasswordField({onPasswordChanged, onKeyDown} : PasswordFieldProps){
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
const iconSize = 20;
const changePassword = (password : string) => { const changePassword = (password : string) => {
setPassword(password); setPassword(password);
@ -28,13 +28,14 @@ export default function PasswordField({onPasswordChanged, onKeyDown} : PasswordF
onChange={(e) => changePassword(e.target.value)} onChange={(e) => changePassword(e.target.value)}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
/> />
{/* Add a little eye icon to the right for showing password */}
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700" className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
aria-label={showPassword ? "Passwort ausblenden" : "Passwort anzeigen"} aria-label={showPassword ? "Passwort ausblenden" : "Passwort anzeigen"}
> >
{showPassword ? <EyeOff size={iconSize} /> : <Eye size={iconSize} />} {showPassword ? <EyeOff size={defaultIconSize} /> : <Eye size={defaultIconSize} />}
</button> </button>
</div> </div>
); );

View file

@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import SvgIcon, { Icon } from "./SvgIcon" import { Search, X} from "lucide-react";
import { defaultIconSize } from "./SvgIcon";
/** /**
* Custom search field component including a clear search functionality * Custom search field component including a clear search functionality
*/ */
@ -24,7 +25,7 @@ export default function SearchField({onSearchStringChanged} : SearchFieldProps){
<div className="relative"> <div className="relative">
{/* Input of searchfield {/* Input of searchfield
Defines border and behavior. Requires extra padding at both sides to Defines border and behavior. Requires extra padding at both sides to
accomodate the icons accommodate the icons
*/} */}
<input <input
className="input-field pl-10 pr-10" className="input-field pl-10 pr-10"
@ -37,17 +38,17 @@ export default function SearchField({onSearchStringChanged} : SearchFieldProps){
Clears search string on click Clears search string on click
*/} */}
<button <button
className="absolute right-0 inset-y-0 flex items-center -ml-1 mr-3" className="absolute right-0 inset-y-0 flex items-center -ml-1 mr-3 default-icon"
onClick = { () => changeSearchString("") } onClick = { () => changeSearchString("") }
> >
<SvgIcon <X
icon = {Icon.X} size = {defaultIconSize}
/> />
</button> </button>
{/* Left icon: Looking glass */} {/* Left icon: Looking glass */}
<div className="absolute left-0 inset-y-0 flex items-center ml-3"> <div className="absolute left-0 inset-y-0 flex items-center ml-3 default-icon">
<SvgIcon <Search
icon = {Icon.LookingGlass} size = {defaultIconSize}
/> />
</div> </div>
</div> </div>

View file

@ -3,6 +3,7 @@
* @todo replace by lucid react * @todo replace by lucid react
*/ */
export const defaultIconSize = 20;
/** /**
* Enum-like const object+type definition to define icons. * Enum-like const object+type definition to define icons.
* The string corresponds to the path definition of the icon * The string corresponds to the path definition of the icon

View file

@ -4,9 +4,10 @@
import type { IngredientModel } from "../../models/IngredientModel" import type { IngredientModel } from "../../models/IngredientModel"
import type { IngredientGroupModel } from "../../models/IngredientGroupModel" import type { IngredientGroupModel } from "../../models/IngredientGroupModel"
import Button, { ButtonType } from "../basics/Button" import Button from "../basics/Button"
import SvgIcon, { Icon } from "../basics/SvgIcon"
import { IngredientListEditor } from "./IngredientListEditor" import { IngredientListEditor } from "./IngredientListEditor"
import { Plus } from "lucide-react"
import { ButtonType } from "../basics/BasicButtonDefinitions"
type IngredientGroupListEditorProps = { type IngredientGroupListEditorProps = {
ingredientGroupList: IngredientGroupModel[] ingredientGroupList: IngredientGroupModel[]
@ -58,7 +59,7 @@ export function IngredientGroupListEditor({ ingredientGroupList, onChange }: Ing
))} ))}
<Button <Button
onClick={handleAdd} onClick={handleAdd}
icon={Icon.Plus} icon={Plus}
text={"Add Ingredient Group"} text={"Add Ingredient Group"}
buttonType={ButtonType.PrimaryButton} buttonType={ButtonType.PrimaryButton}
/> />

View file

@ -1,6 +1,7 @@
import { Plus, X } from "lucide-react"
import type { IngredientModel } from "../../models/IngredientModel" import type { IngredientModel } from "../../models/IngredientModel"
import Button, { ButtonType } from "../basics/Button" import Button from "../basics/Button"
import { Icon } from "../basics/SvgIcon" import { ButtonType } from "../basics/BasicButtonDefinitions"
/** /**
* Editor for handling the ingredient list * Editor for handling the ingredient list
@ -53,14 +54,14 @@ export function IngredientListEditor({ ingredients, onChange }: IngredientListEd
/> />
<Button <Button
onClick={() => handleRemove(index)} onClick={() => handleRemove(index)}
icon={Icon.X} icon={X}
buttonType={ButtonType.DarkButton} buttonType={ButtonType.DarkButton}
/> />
</div> </div>
))} ))}
<Button <Button
onClick={handleAdd} onClick={handleAdd}
icon={Icon.Plus} icon={Plus}
text={"Add Ingredient"} text={"Add Ingredient"}
buttonType={ButtonType.PrimaryButton} buttonType={ButtonType.PrimaryButton}
/> />

View file

@ -5,8 +5,9 @@
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import type { InstructionStepModel } from "../../models/InstructionStepModel"; import type { InstructionStepModel } from "../../models/InstructionStepModel";
import Button, { ButtonType } from "../basics/Button"; import Button from "../basics/Button";
import { Icon } from "../basics/SvgIcon"; import { X } from "lucide-react"
import { ButtonType } from "../basics/BasicButtonDefinitions";
type InstructionStepDesktopListItemProps = { type InstructionStepDesktopListItemProps = {
id: string | number; id: string | number;
@ -56,9 +57,8 @@ export function InstructionStepDesktopListItem({
<Button <Button
onClick={() => onRemove(index)} onClick={() => onRemove(index)}
icon={Icon.X} icon={X}
buttonType={ButtonType.DarkButton} buttonType={ButtonType.DarkButton}
title="Schritt löschen"
/> />
</div> </div>
); );

View file

@ -16,9 +16,10 @@ import {
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import type { InstructionStepModel } from "../../models/InstructionStepModel"; import type { InstructionStepModel } from "../../models/InstructionStepModel";
import { InstructionStepDesktopListItem } from "./InstructionStepDesktopListItem"; import { InstructionStepDesktopListItem } from "./InstructionStepDesktopListItem";
import { useInstructionStepListEditor } from "./InstructionStepListEditor"; import { instructionStepListEditorMethods } from "./InstructionStepListEditor";
import Button, { ButtonType } from "../basics/Button"; import Button from "../basics/Button";
import { Icon } from "../basics/SvgIcon"; import { Plus } from "lucide-react";
import { ButtonType } from "../basics/BasicButtonDefinitions";
type InstructionStepListDesktopEditorProps = { type InstructionStepListDesktopEditorProps = {
instructionStepList: InstructionStepModel[]; instructionStepList: InstructionStepModel[];
@ -29,7 +30,7 @@ export function InstructionStepListDesktopEditor({
instructionStepList, instructionStepList,
onChange, onChange,
}: InstructionStepListDesktopEditorProps) { }: InstructionStepListDesktopEditorProps) {
const { handleUpdate, handleAdd, handleRemove } = useInstructionStepListEditor( const { handleUpdate, handleAdd, handleRemove } = instructionStepListEditorMethods(
instructionStepList, instructionStepList,
onChange onChange
); );
@ -76,7 +77,7 @@ export function InstructionStepListDesktopEditor({
</DndContext> </DndContext>
<Button <Button
onClick={handleAdd} onClick={handleAdd}
icon={Icon.Plus} icon={Plus}
text="Schritt hinzufügen" text="Schritt hinzufügen"
buttonType={ButtonType.PrimaryButton} buttonType={ButtonType.PrimaryButton}
className="mt-4" className="mt-4"

View file

@ -50,7 +50,7 @@ export function InstructionStepListEditor({
* *
* Provides add/remove/update functionality used by both mobile and desktop editors. * Provides add/remove/update functionality used by both mobile and desktop editors.
*/ */
export function useInstructionStepListEditor( export function instructionStepListEditorMethods(
instructionStepList: InstructionStepModel[], instructionStepList: InstructionStepModel[],
onChange: (steps: InstructionStepModel[]) => void onChange: (steps: InstructionStepModel[]) => void
) { ) {

View file

@ -2,10 +2,11 @@
* Mobile editor using Up/Down buttons for reordering * Mobile editor using Up/Down buttons for reordering
*/ */
import { ArrowDown, ArrowUp, Plus, X } from "lucide-react";
import type { InstructionStepModel } from "../../models/InstructionStepModel"; import type { InstructionStepModel } from "../../models/InstructionStepModel";
import Button, { ButtonType } from "../basics/Button"; import Button from "../basics/Button";
import { Icon } from "../basics/SvgIcon"; import { instructionStepListEditorMethods } from "./InstructionStepListEditor";
import { useInstructionStepListEditor } from "./InstructionStepListEditor"; import { ButtonType } from "../basics/BasicButtonDefinitions";
type InstructionStepListEditorMobileProps = { type InstructionStepListEditorMobileProps = {
instructionStepList: InstructionStepModel[]; instructionStepList: InstructionStepModel[];
@ -16,7 +17,7 @@ export function InstructionStepListMobileEditor({
instructionStepList, instructionStepList,
onChange, onChange,
}: InstructionStepListEditorMobileProps) { }: InstructionStepListEditorMobileProps) {
const { handleUpdate, handleAdd, handleRemove } = useInstructionStepListEditor( const { handleUpdate, handleAdd, handleRemove } = instructionStepListEditorMethods(
instructionStepList, instructionStepList,
onChange onChange
); );
@ -43,12 +44,12 @@ export function InstructionStepListMobileEditor({
</div> </div>
<div className="flex flex-col mt-2"> <div className="flex flex-col mt-2">
<Button <Button
icon={Icon.ArrowUp} icon={ArrowUp}
onClick={() => moveStep(index, "up")} onClick={() => moveStep(index, "up")}
buttonType={ButtonType.TransparentButton} buttonType={ButtonType.TransparentButton}
/> />
<Button <Button
icon={Icon.ArrowDown} icon={ArrowDown}
onClick={() => moveStep(index, "down")} onClick={() => moveStep(index, "down")}
buttonType={ButtonType.TransparentButton} buttonType={ButtonType.TransparentButton}
/> />
@ -69,14 +70,14 @@ export function InstructionStepListMobileEditor({
<Button <Button
onClick={() => handleRemove(index)} onClick={() => handleRemove(index)}
icon={Icon.X} icon={X}
buttonType={ButtonType.DarkButton} buttonType={ButtonType.DarkButton}
/> />
</div> </div>
))} ))}
<Button <Button
onClick={handleAdd} onClick={handleAdd}
icon={Icon.Plus} icon={Plus}
text="Schritt hinzufügen" text="Schritt hinzufügen"
buttonType={ButtonType.PrimaryButton} buttonType={ButtonType.PrimaryButton}
className="mt-4" className="mt-4"

View file

@ -2,9 +2,10 @@ import { useState } from "react"
import type { RecipeModel } from "../../models/RecipeModel" import type { RecipeModel } from "../../models/RecipeModel"
import type { IngredientGroupModel } from "../../models/IngredientGroupModel" import type { IngredientGroupModel } from "../../models/IngredientGroupModel"
import { IngredientGroupListEditor } from "./IngredientGroupListEditor" import { IngredientGroupListEditor } from "./IngredientGroupListEditor"
import Button, { ButtonType } from "../basics/Button" import Button from "../basics/Button"
import { InstructionStepListEditor } from "./InstructionStepListEditor" import { InstructionStepListEditor } from "./InstructionStepListEditor"
import type { InstructionStepModel } from "../../models/InstructionStepModel" import type { InstructionStepModel } from "../../models/InstructionStepModel"
import { ButtonType } from "../basics/BasicButtonDefinitions"
type RecipeEditorProps = { type RecipeEditorProps = {
recipe: RecipeModel recipe: RecipeModel
@ -91,7 +92,7 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
{/* Title */} {/* Title */}
<h3 className="subsection-heading">Title</h3> <h3 className="subsection-heading">Title</h3>
<input <input
className={`input-field ${errors.title ? "border-red-500" : ""}`} className={`input-field ${errors.title ? "error-text" : ""}`}
placeholder="Title" placeholder="Title"
value={draft.title} value={draft.title}
onChange={e => setDraft({ ...draft, title: e.target.value })} onChange={e => setDraft({ ...draft, title: e.target.value })}
@ -123,7 +124,7 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
/> />
</div> </div>
{/* Ingredient List - @todo better visualization of errors! */} {/* Ingredient List - @todo better visualization of errors! */}
<div className={errors.ingredients ? "border border-red-500 rounded p-2" : ""}> <div className={errors.ingredients ? "border error-text rounded p-2" : ""}>
<IngredientGroupListEditor <IngredientGroupListEditor
ingredientGroupList={draft.ingredientGroupList} ingredientGroupList={draft.ingredientGroupList}
onChange={updateIngredientGroupList} onChange={updateIngredientGroupList}

View file

@ -1,4 +1,4 @@
import { ButtonType } from "../basics/Button" import { ButtonType } from "../basics/BasicButtonDefinitions"
import Button from "../basics/Button" import Button from "../basics/Button"
import SearchField from "../basics/SearchField" import SearchField from "../basics/SearchField"