A website inspired by Last.fm that will keep track of your listening statistics
lastfm music statistics
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Changes for the client. Create interaction components such as buttons, inputs and checkboxes. Create authentication pages and layout. Make small changes to navigation.

oscar345 ba2f2755 0d302c0c

+753 -4
+26
web/components/content/Description.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from "svelte"; 3 + import type { HTMLAttributes } from "svelte/elements"; 4 + 5 + type Props = HTMLAttributes<HTMLSpanElement> & { 6 + children: Snippet; 7 + }; 8 + 9 + let { children, ...props }: Props = $props(); 10 + </script> 11 + 12 + <small {...props}> 13 + {@render children()} 14 + </small> 15 + 16 + <style> 17 + small { 18 + font-size: var(--text-sm); 19 + font-weight: var(--font-weight-normal); 20 + line-height: var(--text-xs--line-height); 21 + color: var(--color-content-200); 22 + display: inline-block; 23 + width: fit-content; 24 + max-width: 100%; 25 + } 26 + </style>
+42
web/components/content/Error.svelte
··· 1 + <script lang="ts"> 2 + import type { HTMLAttributes } from "svelte/elements"; 3 + import Icon from "$components/content/Icon.svelte"; 4 + 5 + type Props = HTMLAttributes<HTMLSpanElement> & { 6 + messages?: string[]; 7 + }; 8 + 9 + let { messages, ...props }: Props = $props(); 10 + </script> 11 + 12 + {#if (messages ?? []).length > 0} 13 + <div class="messages"> 14 + {#each messages as message} 15 + <span {...props}> 16 + <Icon name="hero-exclamation-circle-mini" /> 17 + {message} 18 + </span> 19 + {/each} 20 + </div> 21 + {/if} 22 + 23 + <style> 24 + .messages { 25 + display: flex; 26 + flex-direction: column; 27 + gap: var(--spacing-1); 28 + } 29 + 30 + span { 31 + display: inline-flex; 32 + align-items: center; 33 + gap: var(--spacing-1); 34 + color: var(--color-danger); 35 + font-size: var(--text-sm); 36 + line-height: var(--text-sm--line-height); 37 + } 38 + 39 + span > :global(.icon) { 40 + color: var(--color-danger); 41 + } 42 + </style>
+7
web/components/content/Icon.svelte
··· 1 + <script lang="ts"> 2 + type Props = { 3 + name: string; 4 + }; 5 + 6 + let {}: Props = $props(); 7 + </script>
+14
web/components/content/Label.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from "svelte"; 3 + import type { HTMLLabelAttributes } from "svelte/elements"; 4 + 5 + type Props = HTMLLabelAttributes & { 6 + children: Snippet; 7 + }; 8 + 9 + let { children, ...props }: Props = $props(); 10 + </script> 11 + 12 + <label class="label" {...props}> 13 + {@render children()} 14 + </label>
+40
web/components/interaction/Button.svelte
··· 1 + <script lang="ts"> 2 + import Icon from "$components/content/Icon.svelte"; 3 + import type { ButtonProps } from "$lib/types"; 4 + 5 + let { 6 + children, 7 + icon, 8 + shape = "square", 9 + scheme = "default", 10 + variant = "default", 11 + icon_position = "left", 12 + class: class_, 13 + ...rest 14 + }: ButtonProps = $props(); 15 + 16 + let classes = $derived([ 17 + scheme === "default" && "scheme-default", 18 + scheme === "primary" && "scheme-primary", 19 + variant === "default" && "variant-default", 20 + variant === "text" && "variant-text", 21 + shape === "square" && "shape-square", 22 + shape === "rounded" && "shape-rounded", 23 + shape === "circle" && "shape-circle", 24 + icon !== undefined && "with-icon", 25 + "button", 26 + class_, 27 + ]); 28 + </script> 29 + 30 + <button class={classes} {...rest}> 31 + {#if icon && icon_position === "left"} 32 + <Icon name={icon} /> 33 + {/if} 34 + {#if children} 35 + {@render children()} 36 + {/if} 37 + {#if icon && icon_position === "right"} 38 + <Icon name={icon} /> 39 + {/if} 40 + </button>
+34
web/components/interaction/Checkbox.svelte
··· 1 + <script lang="ts"> 2 + import { type InputProps } from "$lib/types"; 3 + let randomID = $props.id(); 4 + let { 5 + id = randomID, 6 + checked = $bindable(), 7 + ...props 8 + }: InputProps = $props(); 9 + </script> 10 + 11 + <input {id} type="checkbox" {...props} bind:checked /> 12 + 13 + <style> 14 + input { 15 + height: var(--spacing-4); 16 + width: var(--spacing-4); 17 + border: var(--theme-default-border); 18 + color: color-mix(in srgb, var(--color-accent-700) 75%, white); 19 + background-color: var(--color-base-300); 20 + 21 + &:checked { 22 + border-color: var(--color-base-content); 23 + background-color: var(--color-content-100); 24 + } 25 + 26 + &:focus { 27 + box-shadow: none; 28 + } 29 + 30 + &:focus { 31 + outline: 2px solid var(--color-primary); 32 + } 33 + } 34 + </style>
+129
web/components/interaction/Field.svelte
··· 1 + <script lang="ts"> 2 + import type { InputProps, SelectProps } from "$lib/types"; 3 + import type { Snippet } from "svelte"; 4 + import Select from "$components/interaction/Select.svelte"; 5 + import Input from "$components/interaction/Input.svelte"; 6 + import Label from "$components/content/Label.svelte"; 7 + import Error from "$components/content/Error.svelte"; 8 + import Description from "$components/content/Description.svelte"; 9 + import Checkbox from "./Checkbox.svelte"; 10 + 11 + let randomID = $props.id(); 12 + 13 + type Content = Snippet | string; 14 + 15 + type BaseProps = { 16 + label?: Content | undefined; 17 + errors?: string[] | undefined; 18 + description?: Content | undefined; 19 + children?: Snippet | undefined; 20 + checked?: boolean; 21 + }; 22 + 23 + type Props = 24 + | ({ as: "select" } & SelectProps & BaseProps) 25 + | ({ as: "input" } & InputProps & BaseProps) 26 + | ({ as: "checkbox" } & InputProps & BaseProps); 27 + 28 + let { 29 + label: label, 30 + errors: errors, 31 + description: description, 32 + as: as, 33 + id = randomID, 34 + value = $bindable(), 35 + checked = $bindable(), 36 + ...props 37 + }: Props = $props(); 38 + </script> 39 + 40 + <div class={[as, "field"]}> 41 + <div class="label"> 42 + {#if label} 43 + {#if typeof label === "string"} 44 + <Label for={id}>{label}</Label> 45 + {:else} 46 + {@render label()} 47 + {/if} 48 + {/if} 49 + </div> 50 + 51 + <div class="interaction"> 52 + {#if as === "select"} 53 + <Select 54 + {id} 55 + aria-describedby={description !== undefined && id + "-description"} 56 + {...props as SelectProps} 57 + /> 58 + {:else if as === "input"} 59 + <Input 60 + {id} 61 + bind:value 62 + aria-describedby={description !== undefined && id + "-description"} 63 + {...props as InputProps} 64 + /> 65 + {:else if as === "checkbox"} 66 + <Checkbox 67 + {id} 68 + bind:checked 69 + aria-describedby={description !== undefined && id + "-description"} 70 + {...props as InputProps} 71 + /> 72 + {/if} 73 + </div> 74 + 75 + {#if description} 76 + <div class="description"> 77 + {#if typeof description === "string"} 78 + <Description id={id + "-description"}>{description}</Description> 79 + {:else} 80 + {@render description()} 81 + {/if} 82 + </div> 83 + {/if} 84 + 85 + {#if (errors ?? []).length > 0} 86 + <div class="error"> 87 + <Error messages={errors} /> 88 + </div> 89 + {/if} 90 + </div> 91 + 92 + <style> 93 + .field { 94 + display: grid; 95 + grid-template-columns: var(--spacing-4) 1fr; 96 + column-gap: var(--spacing-2); 97 + row-gap: var(--spacing-0_5); 98 + } 99 + 100 + .label { 101 + grid-column: 1 / 3; 102 + } 103 + 104 + .field.checkbox > .label { 105 + grid-row: 1 / 2; 106 + grid-column: 2 / 3; 107 + align-self: center; 108 + } 109 + 110 + .interaction { 111 + grid-column: 1 / 3; 112 + } 113 + 114 + .field.checkbox .interaction { 115 + grid-column: 1 / 2; 116 + grid-row: 1 / 2; 117 + height: var(--spacing-4); 118 + align-self: center; 119 + display: flex; 120 + } 121 + 122 + .description { 123 + grid-column: 1 / 3; 124 + } 125 + 126 + .error { 127 + grid-column: 1 / 3; 128 + } 129 + </style>
+65
web/components/interaction/Input.svelte
··· 1 + <script lang="ts"> 2 + import { type InputProps } from "$lib/types"; 3 + let randomID = $props.id(); 4 + let { 5 + name, 6 + id = randomID, 7 + value = $bindable(), 8 + ...props 9 + }: InputProps = $props(); 10 + </script> 11 + 12 + <input {name} {id} {...props} bind:value /> 13 + 14 + <style> 15 + input { 16 + font-weight: var(--font-weight-normal); 17 + color: var(--color-content-100); 18 + display: block; 19 + border-style: solid; 20 + border-color: var(--color-muted-200); 21 + border-width: 1px; 22 + padding-inline: var(--spacing-3); 23 + height: var(--spacing-10); 24 + width: 100%; 25 + box-shadow: var(--shadow-xs); 26 + background-color: color-mix( 27 + in srgb, 28 + var(--color-base-100), 29 + var(--mixin-color-light-200) 30 + ); 31 + font-size: var(--text-base); 32 + line-height: var(--text-base--line-height); 33 + 34 + @media (prefers-color-scheme: dark) { 35 + background-color: color-mix( 36 + in srgb, 37 + var(--color-base-100), 38 + var(--mixin-color-dark-100) 39 + ); 40 + } 41 + 42 + @media (width >= 40rem) { 43 + height: var(--spacing-10); 44 + font-size: var(--text-sm); 45 + line-height: var(--spacing-6); 46 + } 47 + 48 + outline-offset: 0px; 49 + &:focus, 50 + &:focus-visible { 51 + outline: 1px solid var(--color-primary); 52 + border-color: var(--color-primary); 53 + } 54 + 55 + &[aria-invalid="true"] { 56 + border-color: var(--color-error); 57 + } 58 + 59 + &:disabled { 60 + color: var(--color-muted-300); 61 + border-color: var(--color-muted-100); 62 + cursor: not-allowed; 63 + } 64 + } 65 + </style>
+52
web/components/interaction/Link.svelte
··· 1 + <script lang="ts"> 2 + import Icon from "$components/content/Icon.svelte"; 3 + import type { LinkProps } from "$lib/types"; 4 + import { Link } from "@inertiajs/svelte"; 5 + 6 + let { 7 + children, 8 + icon, 9 + href, 10 + shape = "square", 11 + scheme = "default", 12 + variant = "default", 13 + icon_position = "left", 14 + class: class_, 15 + ...rest 16 + }: LinkProps = $props(); 17 + 18 + let classes = $derived([ 19 + scheme === "default" && "scheme-default", 20 + scheme === "primary" && "scheme-primary", 21 + variant === "default" && "variant-default", 22 + variant === "text" && "variant-text", 23 + shape === "square" && "shape-square", 24 + shape === "rounded" && "shape-rounded", 25 + shape === "circle" && "shape-circle", 26 + icon !== undefined && "with-icon", 27 + "button", 28 + class_, 29 + ]); 30 + </script> 31 + 32 + {#snippet content()} 33 + {#if icon && icon_position === "left"} 34 + <Icon name={icon} /> 35 + {/if} 36 + {#if children} 37 + {@render children()} 38 + {/if} 39 + {#if icon && icon_position === "right"} 40 + <Icon name={icon} /> 41 + {/if} 42 + {/snippet} 43 + 44 + {#if typeof href === "string"} 45 + <a class={classes} {...rest}> 46 + {@render content()} 47 + </a> 48 + {:else} 49 + <Link class={classes} {...rest}> 50 + {@render content()} 51 + </Link> 52 + {/if}
+77
web/components/interaction/Select.svelte
··· 1 + <script lang="ts"> 2 + import type { SelectProps } from "$lib/types"; 3 + 4 + let randomID = $props.id(); 5 + 6 + let { 7 + name, 8 + id = randomID, 9 + value = $bindable(), 10 + values, 11 + ...props 12 + }: SelectProps = $props(); 13 + </script> 14 + 15 + <select {name} {id} bind:value {...props}> 16 + {#each values as item} 17 + <option 18 + value={item.value} 19 + disabled={item.disabled} 20 + selected={item.current}>{item.label}</option 21 + > 22 + {/each} 23 + </select> 24 + 25 + <style> 26 + select { 27 + font-weight: var(--font-weight-normal); 28 + color: var(--color-content-100); 29 + display: block; 30 + border-style: solid; 31 + border-color: var(--color-muted-200); 32 + border-width: 1px; 33 + padding-inline: var(--spacing-3); 34 + height: var(--spacing-10); 35 + width: 100%; 36 + box-shadow: var(--shadow-xs); 37 + border-radius: var(--radius-interactive); 38 + background-color: color-mix( 39 + in srgb, 40 + var(--color-base-100), 41 + var(--mixin-color-light-200) 42 + ); 43 + font-size: var(--text-base); 44 + line-height: var(--text-base--line-height); 45 + 46 + @media (prefers-color-scheme: dark) { 47 + background-color: color-mix( 48 + in srgb, 49 + var(--color-base-100), 50 + var(--mixin-color-dark-100) 51 + ); 52 + } 53 + 54 + @media (width >= 40rem) { 55 + height: var(--spacing-10); 56 + font-size: var(--text-sm); 57 + line-height: var(--spacing-6); 58 + } 59 + 60 + outline-offset: 0px; 61 + &:focus, 62 + &:focus-visible { 63 + outline: 1px solid var(--color-primary); 64 + border-color: var(--color-primary); 65 + } 66 + 67 + &[aria-invalid="true"] { 68 + border-color: var(--color-error); 69 + } 70 + 71 + &:disabled { 72 + color: var(--color-muted-300); 73 + border-color: var(--color-muted-100); 74 + cursor: not-allowed; 75 + } 76 + } 77 + </style>
web/components/interaction/Switch.svelte

This is a binary file and will not be displayed.

web/components/interaction/Textarea.svelte

This is a binary file and will not be displayed.

+49
web/components/layouts/authentication/Layout.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from "svelte"; 3 + 4 + type Props = { 5 + children: Snippet; 6 + }; 7 + 8 + let { children }: Props = $props(); 9 + </script> 10 + 11 + <main> 12 + <section> 13 + {@render children()} 14 + </section> 15 + </main> 16 + 17 + <style> 18 + main { 19 + background-color: var(--color-primary); 20 + display: grid; 21 + place-items: center; 22 + height: 100vh; 23 + 24 + @media (width < 48rem) { 25 + padding: var(--spacing-4); 26 + } 27 + 28 + @media (width >= 48rem) { 29 + justify-items: end; 30 + } 31 + } 32 + 33 + section { 34 + display: flex; 35 + flex-direction: column; 36 + justify-content: center; 37 + gap: var(--spacing-4); 38 + box-shadow: var(--shadow-lg); 39 + background-color: var(--color-base-300); 40 + padding: var(--spacing-6); 41 + width: 100%; 42 + max-width: var(--width-lg); 43 + 44 + @media (width >= 48rem) { 45 + padding: var(--spacing-8); 46 + height: 100%; 47 + } 48 + } 49 + </style>
-1
web/components/navigation/library/Navigation.svelte
··· 56 56 li > :global(a) { 57 57 color: var(--color-primary); 58 58 padding-block: var(--spacing-0_5); 59 - border-radius: var(--radius-md); 60 59 font-size: var(--text-sm); 61 60 line-height: var(--text-sm--line-height); 62 61 font-weight: var(--font-weight-medium);
+2 -3
web/components/navigation/web/Navigation.svelte
··· 8 8 { href: GET_Index(), label: "Home", view: "Index" }, 9 9 { href: GET_Mixtapes(), label: "Mixtapes", view: "mixtapes/Index" }, 10 10 { href: GET_Friends(), label: "Friends", view: "friends/Index" }, 11 - { href: GET_Library(), label: "Library", view: "library/Index" }, 11 + { href: GET_Library(), label: "Library", view: "library" }, 12 12 ]); 13 13 </script> 14 14 ··· 22 22 <li> 23 23 <Link 24 24 href={item.href} 25 - aria-current={$page.component === item.view 25 + aria-current={$page.component.startsWith(item.view) 26 26 ? "page" 27 27 : undefined} 28 28 > ··· 61 61 li > :global(a) { 62 62 color: var(--color-primary); 63 63 padding: var(--spacing-2) var(--spacing-1_5); 64 - border-radius: var(--radius-md); 65 64 font-size: var(--text-sm); 66 65 line-height: var(--text-sm--line-height); 67 66 font-weight: var(--font-weight-medium);
+12
web/global.d.ts
··· 1 + declare module "@inertiajs/core" { 2 + export interface InertiaConfig { 3 + // sharedPageProps: { 4 + // auth: { user: { id: number; name: string } | null }; 5 + // appName: string; 6 + // }; 7 + // flashDataType: { 8 + // toast?: { type: "success" | "error"; message: string }; 9 + // }; 10 + errorValueType: string[]; 11 + } 12 + }
+24
web/lib/.gen/routes.ts
··· 17 17 } 18 18 19 19 20 + export function GET_AuthenticationLogin() : { url: string, method: Method} { 21 + return { 22 + url: `/authentication/login`, 23 + method: "get" 24 + } 25 + } 26 + 27 + 28 + export function POST_AuthenticationLogin() : { url: string, method: Method} { 29 + return { 30 + url: `/authentication/login`, 31 + method: "post" 32 + } 33 + } 34 + 35 + 20 36 export function GET_Friends() : { url: string, method: Method} { 21 37 return { 22 38 url: `/friends`, ··· 48 64 } 49 65 } 50 66 67 + 68 + export function GET_Test() : { url: string, method: Method} { 69 + return { 70 + url: `/test`, 71 + method: "get" 72 + } 73 + } 74 +
+3
web/styles/app.css
··· 4 4 @import "tailwindcss/preflight.css" layer(base); 5 5 @import "tailwindcss/utilities.css" layer(utilities); 6 6 7 + @plugin "@tailwindcss/forms"; 8 + 7 9 @import "./colors.css" layer(theme); 10 + @import "./button.css" layer(components); 8 11 @import "./typography.css" layer(components); 9 12 10 13 @utility container {
+100
web/styles/button.css
··· 1 + .button { 2 + font-size: var(--text-sm); 3 + font-weight: var(--font-weight-medium); 4 + display: inline-flex; 5 + align-items: center; 6 + justify-content: center; 7 + text-align: center; 8 + } 9 + 10 + .button.variant-default { 11 + gap: var(--spacing-1); 12 + line-height: var(--text-sm--line-height); 13 + height: var(--spacing-10); 14 + 15 + padding-inline: var(--padding-inline); 16 + border: 1px solid hsl(from var(--text-color) h s l / 0.5); 17 + 18 + background-color: var(--bg-color); 19 + color: var(--text-color); 20 + 21 + &:hover:not(:where(:disabled, .state-disabled)) { 22 + background-color: color-mix(in srgb, var(--bg-color), var(--mixin-bg)); 23 + } 24 + 25 + &:active { 26 + color: color-mix(in srgb, var(--text-color), var(--mixin-text)); 27 + } 28 + 29 + &:disabled, 30 + &.state-disabled { 31 + pointer-events: none; 32 + color: color-mix( 33 + in srgb, 34 + var(--text-color), 35 + var(--mixin-text-disabled) 36 + ); 37 + background-color: color-mix( 38 + in srgb, 39 + var(--bg-color), 40 + var(--mixin-bg-disabled) 41 + ); 42 + 43 + &:hover { 44 + cursor: not-allowed; 45 + } 46 + } 47 + 48 + &.with-icon > .icon { 49 + width: var(--spacing-5); 50 + height: var(--spacing-5); 51 + } 52 + } 53 + 54 + .button.variant-text { 55 + gap: var(--spacing-0_5); 56 + line-height: var(--leading-tight); 57 + height: var(--spacing-4); 58 + } 59 + 60 + .button.variant-text > .icon { 61 + margin-bottom: -2px; 62 + } 63 + 64 + .button.shape-square { 65 + --border-radius: var(--radius-md); 66 + --padding-inline: var(--spacing-3); 67 + } 68 + 69 + .button.shape-rounded { 70 + --border-radius: 9999px; 71 + --padding-inline: var(--spacing-3_5); 72 + } 73 + 74 + .button.shape-circle { 75 + --border-radius: 9999px; 76 + --padding-inline: var(--spacing-1); 77 + aspect-ratio: 1 / 1; 78 + width: var(--spacing-9); 79 + } 80 + 81 + .button.scheme-default { 82 + --bg-color: var(--color-secondary); 83 + --text-color: var(--color-secondary-contrast); 84 + 85 + --mixin-text: var(--mixin-color-light-200); 86 + --mixin-bg: var(--mixin-color-light-100); 87 + 88 + --mixin-text-disabled: var(--mixin-color-light-200); 89 + --mixin-bg-disabled: var(--mixin-color-light-100); 90 + } 91 + 92 + .button.scheme-primary { 93 + --bg-color: var(--color-primary); 94 + --text-color: var(--color-primary-contrast); 95 + 96 + --mixin-text: var(--mixin-color-dark-100); 97 + --mixin-bg: var(--mixin-color-light-100); 98 + 99 + --mixin-text-disabled: var(--mixin-color-dark-200); 100 + }
+9
web/styles/typography.css
··· 32 32 display: flex; 33 33 flex-direction: column; 34 34 } 35 + 36 + .label { 37 + font-size: var(--text-sm); 38 + font-weight: var(--font-weight-medium); 39 + line-height: var(--text-xs--line-height); 40 + color: var(--color-content-100); 41 + width: fit-content; 42 + max-width: 100%; 43 + }
+63
web/views/authentication/Login.svelte
··· 1 + <script module lang="ts"> 2 + import Button from "$components/interaction/Button.svelte"; 3 + import Field from "$components/interaction/Field.svelte"; 4 + import { default as Base } from "$components/layouts/Layout.svelte"; 5 + import { default as Authentication } from "$components/layouts/authentication/Layout.svelte"; 6 + import { POST_AuthenticationLogin } from "$routes"; 7 + import { useForm } from "@inertiajs/svelte"; 8 + 9 + export const layout = [Base, Authentication]; 10 + </script> 11 + 12 + <script lang="ts"> 13 + type Props = {}; 14 + 15 + let {}: Props = $props(); 16 + 17 + const form = useForm({ 18 + username: "", 19 + password: "", 20 + }); 21 + 22 + function submit(e?: SubmitEvent) { 23 + e?.preventDefault(); 24 + $form.submit(POST_AuthenticationLogin(), {}); 25 + } 26 + </script> 27 + 28 + <header class="header"> 29 + <hgroup> 30 + <h1 class="h1">Login</h1> 31 + <p class="subtitle"> 32 + Lorem ipsum dolor sit, amet consectetur adipisicing elit. 33 + </p> 34 + </hgroup> 35 + </header> 36 + 37 + <form onsubmit={submit}> 38 + <Field 39 + as="input" 40 + type="text" 41 + label="Username" 42 + bind:value={$form.username} 43 + description="Enter your username" 44 + errors={$form.errors.username} 45 + /> 46 + <Field 47 + as="input" 48 + type="password" 49 + label="Password" 50 + bind:value={$form.password} 51 + description="Enter your password" 52 + /> 53 + <Field as="checkbox" label="Remember me" /> 54 + <Button scheme="primary">submit</Button> 55 + </form> 56 + 57 + <style> 58 + form { 59 + display: flex; 60 + flex-direction: column; 61 + gap: var(--spacing-4); 62 + } 63 + </style>
+5
web/views/authentication/Register.svelte
··· 1 + <script lang="ts"> 2 + type Props = {}; 3 + 4 + let {}: Props = $props(); 5 + </script>