- Rename Grimaired -> Grimoired everywhere (title, nav, descriptions, token keys) - Update domain from skills.here.run.place to grimoi.red - Add Grimoired logo with description on homepage - Add accordion behavior for Quick install / Quick push sections - Add generic resource system (skills, agents, output-styles, rules) - Add resource registry, editor, search, and file manager components
145 lines
5.2 KiB
Vue
145 lines
5.2 KiB
Vue
<template>
|
|
<!-- Edit button -->
|
|
<button
|
|
@click="handleClick"
|
|
class="inline-flex items-center gap-1.5 rounded-lg border border-white/[0.08] bg-surface-200 px-3.5 py-2 text-sm font-medium text-gray-300 hover:border-white/[0.15] hover:text-white transition-all"
|
|
>
|
|
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
|
</svg>
|
|
Edit
|
|
</button>
|
|
|
|
<!-- Modal backdrop -->
|
|
<Teleport to="body">
|
|
<div v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showModal = false">
|
|
<div class="w-full max-w-md rounded-2xl border border-white/[0.08] bg-[var(--color-surface-200)] p-6 shadow-2xl">
|
|
<h3 class="text-lg font-semibold text-white mb-1">Author Verification</h3>
|
|
<p class="text-sm text-gray-500 mb-4">
|
|
This resource is owned by <strong class="text-gray-300">{{ authorName || authorEmail }}</strong>. Enter your token to edit.
|
|
</p>
|
|
|
|
<form @submit.prevent="verify">
|
|
<input
|
|
ref="tokenInput"
|
|
v-model="token"
|
|
type="password"
|
|
placeholder="Paste your author token..."
|
|
class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white placeholder-gray-600 font-mono focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"
|
|
/>
|
|
<p v-if="error" class="mt-2 text-sm text-red-400">{{ error }}</p>
|
|
|
|
<div class="mt-4 flex items-center gap-3">
|
|
<button
|
|
type="submit"
|
|
:disabled="verifying || !token"
|
|
class="inline-flex items-center gap-2 rounded-xl bg-[var(--color-accent-500)] px-5 py-2 text-sm font-semibold text-white shadow-lg shadow-[var(--color-accent-500)]/20 hover:bg-[var(--color-accent-600)] disabled:opacity-50 active:scale-[0.97] transition-all"
|
|
>
|
|
<svg v-if="verifying" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>
|
|
{{ verifying ? 'Verifying...' : 'Continue to Edit' }}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
@click="forkResource"
|
|
class="text-sm text-[var(--color-accent-400)] hover:text-[var(--color-accent-300)] transition-colors"
|
|
>
|
|
Fork instead
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
@click="showModal = false"
|
|
class="ml-auto text-sm text-gray-600 hover:text-gray-300 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, nextTick } from 'vue';
|
|
|
|
const props = defineProps<{
|
|
slug: string;
|
|
authorEmail?: string;
|
|
authorName?: string;
|
|
authorHasToken?: boolean;
|
|
resourceType?: string;
|
|
}>();
|
|
|
|
const type = props.resourceType || 'skills';
|
|
|
|
const showModal = ref(false);
|
|
const token = ref('');
|
|
const error = ref('');
|
|
const verifying = ref(false);
|
|
const tokenInput = ref<HTMLInputElement>();
|
|
|
|
async function handleClick() {
|
|
if (!props.authorEmail || !props.authorHasToken) {
|
|
window.location.href = `/${type}/${props.slug}/edit`;
|
|
return;
|
|
}
|
|
|
|
const saved = localStorage.getItem('grimoired-token') || '';
|
|
if (saved) {
|
|
try {
|
|
const res = await fetch('/api/auth/verify', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: props.authorEmail, token: saved }),
|
|
});
|
|
if (res.ok) {
|
|
localStorage.setItem('grimoired-token', saved);
|
|
window.location.href = `/${type}/${props.slug}/edit`;
|
|
return;
|
|
}
|
|
} catch { /* fall through to modal */ }
|
|
}
|
|
|
|
showModal.value = true;
|
|
error.value = '';
|
|
token.value = '';
|
|
nextTick(() => tokenInput.value?.focus());
|
|
}
|
|
|
|
async function verify() {
|
|
verifying.value = true;
|
|
error.value = '';
|
|
|
|
try {
|
|
const res = await fetch('/api/auth/verify', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: props.authorEmail, token: token.value }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
error.value = data.error || 'Invalid token';
|
|
return;
|
|
}
|
|
|
|
localStorage.setItem('grimoired-token', token.value);
|
|
window.location.href = `/${type}/${props.slug}/edit`;
|
|
} catch {
|
|
error.value = 'Could not verify token';
|
|
} finally {
|
|
verifying.value = false;
|
|
}
|
|
}
|
|
|
|
function forkResource() {
|
|
showModal.value = false;
|
|
window.location.href = `/${type}/new?from=${encodeURIComponent(props.slug)}`;
|
|
}
|
|
</script>
|