Icon refactor
This commit is contained in:
27
src/components/Icon.astro
Normal file
27
src/components/Icon.astro
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
import { icons, type IconName } from "../config/icons";
|
||||
|
||||
interface Props {
|
||||
name: IconName;
|
||||
class?: string;
|
||||
"class:list"?: any;
|
||||
}
|
||||
|
||||
const { name, class: className, "class:list": classList } = Astro.props;
|
||||
const svg = icons[name];
|
||||
|
||||
if (!svg) {
|
||||
throw new Error(`Icon "${name}" not found in icon registry`);
|
||||
}
|
||||
---
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="none"
|
||||
class:list={[className, classList]}
|
||||
aria-hidden="true"
|
||||
set:html={svg}
|
||||
/>
|
||||
30
src/components/Icon.vue
Normal file
30
src/components/Icon.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { icons, type IconName } from "../config/icons";
|
||||
|
||||
const props = defineProps<{
|
||||
name: IconName;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const svg = computed(() => {
|
||||
const icon = icons[props.name];
|
||||
if (!icon) {
|
||||
throw new Error(`Icon "${props.name}" not found in icon registry`);
|
||||
}
|
||||
return icon;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="none"
|
||||
:class="props.class"
|
||||
aria-hidden="true"
|
||||
v-html="svg"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import Icon from "./Icon.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
clients: { id: string; name: string }[];
|
||||
@@ -164,37 +165,13 @@ function clearForm() {
|
||||
|
||||
<!-- Success Message -->
|
||||
<div v-if="success" class="alert alert-success">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="check-circle" class="stroke-current shrink-0 h-6 w-6" />
|
||||
<span>Manual time entry created successfully!</span>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div v-if="error" class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="x-circle" class="stroke-current shrink-0 h-6 w-6" />
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import Icon from './Icon.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
||||
@@ -33,7 +33,7 @@ const chartData = computed(() => ({
|
||||
{
|
||||
data: props.tags.map((t) => t.totalTime / (1000 * 60)), // Convert to minutes
|
||||
backgroundColor: props.tags.map((t) => t.color),
|
||||
borderColor: "#1e293b", // Matches typical dark mode bg
|
||||
borderColor: "#1e293b",
|
||||
borderWidth: 2,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import Icon from './Icon.vue';
|
||||
|
||||
const theme = ref('macchiato');
|
||||
|
||||
@@ -27,7 +27,7 @@ function toggleTheme() {
|
||||
aria-label="Toggle Theme"
|
||||
>
|
||||
<Icon
|
||||
:icon="theme === 'macchiato' ? 'heroicons:moon' : 'heroicons:sun'"
|
||||
:name="theme === 'macchiato' ? 'moon' : 'sun'"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import Icon from "./Icon.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
initialRunningEntry: {
|
||||
@@ -188,7 +188,7 @@ async function stopTimer() {
|
||||
@click="startTimer"
|
||||
class="btn btn-primary btn-lg min-w-40 shadow-lg shadow-primary/20 hover:shadow-xl hover:shadow-primary/30 transition-all"
|
||||
>
|
||||
<Icon icon="heroicons:play" class="w-5 h-5" />
|
||||
<Icon name="play" class="w-5 h-5" />
|
||||
Start Timer
|
||||
</button>
|
||||
<button
|
||||
@@ -196,7 +196,7 @@ async function stopTimer() {
|
||||
@click="stopTimer"
|
||||
class="btn btn-error btn-lg min-w-40 shadow-lg shadow-error/20 hover:shadow-xl hover:shadow-error/30 transition-all"
|
||||
>
|
||||
<Icon icon="heroicons:stop" class="w-5 h-5" />
|
||||
<Icon name="stop" class="w-5 h-5" />
|
||||
Stop Timer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import Icon from "../Icon.vue";
|
||||
import { startAuthentication } from "@simplewebauthn/browser";
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -60,12 +60,12 @@ async function handlePasskeyLogin() {
|
||||
:disabled="loading"
|
||||
>
|
||||
<span v-if="loading" class="loading loading-spinner loading-sm"></span>
|
||||
<Icon v-else icon="heroicons:finger-print" class="w-5 h-5 mr-2" />
|
||||
<Icon v-else name="finger-print" class="w-5 h-5 mr-2" />
|
||||
Sign in with Passkey
|
||||
</button>
|
||||
|
||||
<div v-if="error" role="alert" class="alert alert-error mt-4">
|
||||
<Icon icon="heroicons:exclamation-circle" class="w-6 h-6" />
|
||||
<Icon name="exclamation-circle" class="w-6 h-6" />
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import Icon from "../Icon.vue";
|
||||
|
||||
interface ApiToken {
|
||||
id: string;
|
||||
@@ -112,7 +112,7 @@ function closeShowTokenModal() {
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="card-title text-lg sm:text-xl">
|
||||
<Icon
|
||||
icon="heroicons:code-bracket-square"
|
||||
name="code-bracket-square"
|
||||
class="w-5 h-5 sm:w-6 sm:h-6"
|
||||
/>
|
||||
API Tokens
|
||||
@@ -121,7 +121,7 @@ function closeShowTokenModal() {
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="createModalOpen = true"
|
||||
>
|
||||
<Icon icon="heroicons:plus" class="w-4 h-4" />
|
||||
<Icon name="plus" class="w-4 h-4" />
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
@@ -161,7 +161,7 @@ function closeShowTokenModal() {
|
||||
class="btn btn-ghost btn-xs text-error"
|
||||
@click="deleteToken(token.id)"
|
||||
>
|
||||
<Icon icon="heroicons:trash" class="w-4 h-4" />
|
||||
<Icon name="trash" class="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -222,7 +222,7 @@ function closeShowTokenModal() {
|
||||
<dialog class="modal" :class="{ 'modal-open': showTokenModalOpen }">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg text-success flex items-center gap-2">
|
||||
<Icon icon="heroicons:check-circle" class="w-6 h-6" />
|
||||
<Icon name="check-circle" class="w-6 h-6" />
|
||||
Token Created
|
||||
</h3>
|
||||
<p class="py-4">
|
||||
@@ -239,7 +239,7 @@ function closeShowTokenModal() {
|
||||
@click="copyToken"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<Icon icon="heroicons:clipboard" class="w-4 h-4" />
|
||||
<Icon name="clipboard" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import Icon from "../Icon.vue";
|
||||
import { startRegistration } from "@simplewebauthn/browser";
|
||||
|
||||
interface Passkey {
|
||||
@@ -98,7 +98,7 @@ async function deletePasskey(id: string) {
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="card-title text-lg sm:text-xl">
|
||||
<Icon icon="heroicons:finger-print" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
<Icon name="finger-print" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
Passkeys
|
||||
</h2>
|
||||
<button
|
||||
@@ -110,7 +110,7 @@ async function deletePasskey(id: string) {
|
||||
v-if="loading"
|
||||
class="loading loading-spinner loading-xs"
|
||||
></span>
|
||||
<Icon v-else icon="heroicons:plus" class="w-4 h-4" />
|
||||
<Icon v-else name="plus" class="w-4 h-4" />
|
||||
Add Passkey
|
||||
</button>
|
||||
</div>
|
||||
@@ -157,7 +157,7 @@ async function deletePasskey(id: string) {
|
||||
class="btn btn-ghost btn-xs text-error"
|
||||
@click="deletePasskey(pk.id)"
|
||||
>
|
||||
<Icon icon="heroicons:trash" class="w-4 h-4" />
|
||||
<Icon name="trash" class="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import Icon from "../Icon.vue";
|
||||
|
||||
const currentPassword = ref("");
|
||||
const newPassword = ref("");
|
||||
@@ -76,10 +76,10 @@ async function changePassword() {
|
||||
]"
|
||||
>
|
||||
<Icon
|
||||
:icon="
|
||||
:name="
|
||||
message.type === 'success'
|
||||
? 'heroicons:check-circle'
|
||||
: 'heroicons:exclamation-circle'
|
||||
? 'check-circle'
|
||||
: 'exclamation-circle'
|
||||
"
|
||||
class="w-6 h-6 shrink-0"
|
||||
/>
|
||||
@@ -89,7 +89,7 @@ async function changePassword() {
|
||||
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
||||
<Icon icon="heroicons:key" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
<Icon name="key" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
Change Password
|
||||
</h2>
|
||||
|
||||
@@ -163,7 +163,7 @@ async function changePassword() {
|
||||
v-if="loading"
|
||||
class="loading loading-spinner loading-sm"
|
||||
></span>
|
||||
<Icon v-else icon="heroicons:lock-closed" class="w-5 h-5" />
|
||||
<Icon v-else name="lock-closed" class="w-5 h-5" />
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import Icon from "../Icon.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
user: {
|
||||
@@ -61,10 +61,10 @@ async function updateProfile() {
|
||||
]"
|
||||
>
|
||||
<Icon
|
||||
:icon="
|
||||
:name="
|
||||
message.type === 'success'
|
||||
? 'heroicons:check-circle'
|
||||
: 'heroicons:exclamation-circle'
|
||||
? 'check-circle'
|
||||
: 'exclamation-circle'
|
||||
"
|
||||
class="w-6 h-6 shrink-0"
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ async function updateProfile() {
|
||||
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
||||
<Icon icon="heroicons:user-circle" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
<Icon name="user-circle" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
Profile Information
|
||||
</h2>
|
||||
|
||||
@@ -128,7 +128,7 @@ async function updateProfile() {
|
||||
v-if="loading"
|
||||
class="loading loading-spinner loading-sm"
|
||||
></span>
|
||||
<Icon v-else icon="heroicons:check" class="w-5 h-5" />
|
||||
<Icon v-else name="check" class="w-5 h-5" />
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user