Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .vitepress/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,39 @@
* in custom container, badges, etc.
* -------------------------------------------------------------------------- */

@theme {
--color-primary: var(--vp-c-brand-2);
--color-primary2: var(--vp-c-brand-1);

--color-soft-bg: var(--vp-c-bg-soft);
--color-border: var(--vp-c-border);
--color-bg: var(--vp-c-bg);

--color-text1: var(--vp-c-text-1);
--color-text2: var(--vp-c-text-2);
--color-text3: var(--vp-c-text-3);

--color-success-1: var(--vp-c-green-1);
--color-success-2: var(--vp-c-green-2);
--color-success-3: var(--vp-c-green-3);
--color-success-soft: var(--vp-c-green-soft);

--color-important-1: var(--vp-c-purple-1);
--color-important-2: var(--vp-c-purple-2);
--color-important-3: var(--vp-c-purple-3);
--color-important-soft: var(--vp-c-purple-soft);

--color-warning-1: var(--vp-c-yellow-1);
--color-warning-2: var(--vp-c-yellow-2);
--color-warning-3: var(--vp-c-yellow-3);
--color-warning-soft: var(--vp-c-yellow-soft);

--color-danger-1: var(--vp-c-red-1);
--color-danger-2: var(--vp-c-red-2);
--color-danger-3: var(--vp-c-red-3);
--color-danger-soft: var(--vp-c-red-soft);
}

:root {
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
Expand All @@ -66,6 +99,11 @@
--vp-c-warning-3: var(--vp-c-yellow-3);
--vp-c-warning-soft: var(--vp-c-yellow-soft);

--vp-c-important-1: var(--vp-c-purple-1);
--vp-c-important-2: var(--vp-c-purple-2);
--vp-c-important-3: var(--vp-c-purple-3);
--vp-c-important-soft: var(--vp-c-purple-soft);

--vp-c-danger-1: var(--vp-c-red-1);
--vp-c-danger-2: var(--vp-c-red-2);
--vp-c-danger-3: var(--vp-c-red-3);
Expand Down
45 changes: 45 additions & 0 deletions src/components/ActionButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<component
:is="component"
:href="href"
:target="target"
:rel="rel"
:disabled="disabled"
:class="buttonClasses"
>
<Icon :icon="icon" :height="16" />
{{ label }}
</component>
</template>

<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed } from 'vue';

interface Props {
variant: 'primary' | 'disabled';
label: string;
icon: string;
href?: string;
target?: string;
rel?: string;
disabled?: boolean;
}

const props = defineProps<Props>();

const component = computed(() => {
return props.href ? 'a' : 'button';
});

const buttonClasses = computed(() => {
const baseClasses =
'inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-md text-sm font-medium w-full transition-all duration-200';

if (props.variant === 'primary') {
return `${baseClasses} bg-blue-600 hover:bg-blue-700 text-white hover:-translate-y-0.5`;
} else {
return `${baseClasses} bg-gray-50 text-gray-400 border border-gray-200 cursor-not-allowed`;
}
});
</script>
65 changes: 65 additions & 0 deletions src/components/Badge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<span :class="badgeClasses">
<Icon v-if="icon" :icon="icon" :height="iconSize" />
{{ label }}
</span>
</template>

<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed } from 'vue';

interface Props {
label: string;
variant?:
| 'success'
| 'warning'
| 'danger'
| 'important'
| 'primary'
| 'default';
icon?: string;
iconSize?: number;
size?: 'sm' | 'md';
}

const props = withDefaults(defineProps<Props>(), {
variant: 'default',
iconSize: 14,
size: 'sm',
});

const badgeClasses = computed(() => {
const baseClasses = 'inline-flex items-center rounded-2xl font-medium border';

// Size classes
const sizeClasses =
props.size === 'md'
? 'gap-2 px-4 py-1.5 text-sm'
: 'gap-1.5 px-3 py-1 text-xs';

// Variant classes
let variantClasses = '';
switch (props.variant) {
case 'success':
variantClasses = 'bg-success-soft text-success-3 border-success-3';
break;
case 'warning':
variantClasses = 'bg-warning-soft text-warning-3 border-warning-3';
break;
case 'danger':
variantClasses = 'bg-danger-soft text-danger-3 border-danger-3';
break;
case 'important':
variantClasses = 'bg-important-soft text-important-3 border-important-3';
break;
case 'primary':
variantClasses = 'bg-primary2/10 text-primary2 border-primary2/30';
break;
default:
variantClasses = 'bg-soft-bg text-text2 border-border';
}

return `${baseClasses} ${sizeClasses} ${variantClasses}`;
});
</script>
5 changes: 5 additions & 0 deletions src/components/CardGrid.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div class="my-8 grid grid-cols-1 gap-6 md:grid-cols-2">
<slot />
</div>
</template>
122 changes: 122 additions & 0 deletions src/components/ProjectCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<div
class="bg-soft-bg border-border hover:border-primary relative flex h-full min-h-[220px] flex-col rounded-lg border p-5 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
>
<!-- Icon -->
<div class="flex items-start gap-2">
<div
class="border-border bg-bg flex h-14 w-14 items-center justify-center rounded-xl border"
>
<Icon :icon="icon" height="24" />
</div>
<div>
<h3 class="mt-0! mb-1! text-lg font-semibold">{{ title }}</h3>
<Badge
:label="statusLabel"
:variant="badgeVariant"
:icon="statusIcon"
/>
</div>
</div>

<!-- Content -->
<div class="flex-1">
<p class="text-text2 mb-3 h-14 overflow-hidden text-sm leading-relaxed">
{{ description }}
</p>

<!-- Status Badge -->
</div>
<!-- Actions -->
<div class="mt-auto">
<div v-if="githubUrl" class="grid gap-3">
<Button
as="a"
:href="buttonHref"
:disabled="buttonDisabled"
:target="buttonTarget"
:rel="buttonRel"
class="flex-1"
>
<Icon :icon="buttonIcon" :height="16" />
{{ buttonLabel }}
</Button>
<Button
as="a"
:href="githubUrl"
target="_blank"
rel="noreferrer"
variant="secondary"
class="flex-1"
>
<Icon icon="mdi:github" :height="16" />
{{ githubLabel || 'View Code' }}
</Button>
</div>
<Button
v-else
:as="buttonHref ? 'a' : 'button'"
:href="buttonHref"
:disabled="buttonDisabled"
:target="buttonTarget"
:rel="buttonRel"
class="w-full"
>
<Icon :icon="buttonIcon" :height="16" />
{{ buttonLabel }}
</Button>
</div>
</div>
</template>

<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed } from 'vue';
import Badge from './Badge.vue';
import Button from './ui/Button.vue';

interface Props {
title: string;
description: string;
icon: string;
status: 'available' | 'coming-soon' | 'interactive';
statusLabel: string;
buttonVariant: 'primary' | 'disabled';
buttonLabel: string;
buttonIcon: string;
buttonHref?: string;
buttonTarget?: string;
buttonRel?: string;
buttonDisabled?: boolean;
githubUrl?: string;
githubLabel?: string;
}

const props = withDefaults(defineProps<Props>(), {});

const statusIcon = computed(() => {
switch (props.status) {
case 'available':
return 'mdi:check-circle-outline';
case 'interactive':
return 'mdi:play-circle-outline';
case 'coming-soon':
return 'mdi:clock-outline';
default:
return 'mdi:check-circle-outline';
}
});

const badgeVariant = computed(() => {
switch (props.status) {
case 'available':
return 'success';
case 'coming-soon':
return 'warning';
case 'interactive':
return 'primary';
default:
return 'success';
}
});
</script>
69 changes: 69 additions & 0 deletions src/components/UseCaseCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<div
class="bg-soft-bg group border-border hover:border-primary overflow-hidden rounded-xl border shadow-md transition-all duration-300 hover:-translate-y-1 hover:shadow-xl"
>
<!-- Image -->
<div class="group relative overflow-hidden">
<a :href="demoUrl" target="_blank" rel="noreferrer">
<img
:src="imageUrl"
:alt="imageAlt"
class="max-h-48 w-full object-cover object-top transition-transform duration-300 group-hover:scale-105"
/>
</a>
</div>

<!-- Content -->
<div class="p-6">
<h3 class="text-text1 mt-0! mb-4 text-2xl font-semibold">{{ title }}</h3>
<p class="text-text2 mb-6 text-sm leading-relaxed">{{ description }}</p>

<!-- Feature Tags -->
<div class="mb-6 flex flex-wrap gap-2">
<Badge
v-for="feature in features"
:key="feature"
:label="feature"
variant="primary"
/>
</div>

<!-- Actions -->
<div class="flex flex-wrap gap-4">
<Button as="a" :href="demoUrl" target="_blank" rel="noreferrer">
<Icon :icon="demoIcon" height="18" />
Try Live Demo
</Button>
<Button
as="a"
:href="githubUrl"
target="_blank"
rel="noreferrer"
variant="secondary"
>
<Icon icon="mdi:github" height="18" />
View Code
</Button>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { Icon } from '@iconify/vue';
import Badge from './Badge.vue';
import Button from './ui/Button.vue';

interface Props {
title: string;
description: string;
imageUrl: string;
imageAlt: string;
features: string[];
demoUrl: string;
githubUrl: string;
demoIcon: string;
}

defineProps<Props>();
</script>
Loading