Files
skills-here-run-place/src/components/EditGate.vue
Alejandro Martinez 17423fb3b9 Rename to Grimoired, update domain to grimoi.red, add resource system
- 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
2026-02-13 14:25:07 +01:00

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>