168 lines
No EOL
4.8 KiB
Vue
168 lines
No EOL
4.8 KiB
Vue
<template>
|
|
<div class="flex flex-col space-y-6">
|
|
<div class="flex justify-center">
|
|
<div class="relative group">
|
|
<div
|
|
class="absolute -inset-1 bg-gradient-to-r from-primary to-primary/50 rounded-full opacity-75 group-hover:opacity-100 blur transition duration-300">
|
|
</div>
|
|
<div
|
|
class="relative h-16 w-16 rounded-full bg-gradient-to-br from-muted to-muted/80 flex items-center justify-center shadow-xl group-hover:shadow-2xl transition duration-300">
|
|
<KeyRound class="h-8 w-8 text-foreground group-hover:scale-110 transition duration-300" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-center space-y-2.5">
|
|
<CardTitle class="text-2xl font-bold bg-gradient-to-r from-primary to-primary/80 inline-block text-transparent bg-clip-text">
|
|
Nostr Login
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Login with your Nostr private key or generate a new one
|
|
</CardDescription>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div class="space-y-2">
|
|
<div class="relative">
|
|
<Input
|
|
v-model="privkey"
|
|
type="password"
|
|
placeholder="Enter your private key"
|
|
:class="[
|
|
{ 'border-destructive': error },
|
|
{ 'pr-24': privkey }, // Add padding when we have a value to prevent overlap with button
|
|
]"
|
|
@keyup.enter="login"
|
|
/>
|
|
<Button
|
|
v-if="privkey"
|
|
variant="ghost"
|
|
size="sm"
|
|
class="absolute right-1 top-1 h-8"
|
|
@click="copyKey"
|
|
>
|
|
<Check v-if="copied" class="h-4 w-4 text-green-500" />
|
|
<Copy v-else class="h-4 w-4" />
|
|
<span class="sr-only">Copy private key</span>
|
|
</Button>
|
|
</div>
|
|
<p v-if="error" class="text-sm text-destructive">{{ error }}</p>
|
|
</div>
|
|
|
|
<!-- Recovery message -->
|
|
<div v-if="showRecoveryMessage" class="text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg">
|
|
<p>
|
|
Make sure to save your private key in a secure location. You can use it to recover your chat history on any device.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<Button @click="login" :disabled="!privkey || isLoading">
|
|
<span v-if="isLoading" class="h-4 w-4 animate-spin rounded-full border-2 border-background border-r-transparent" />
|
|
<span v-else>Login</span>
|
|
</Button>
|
|
<Button variant="outline" @click="generateKey">
|
|
Generate New Key
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import { useNostrStore } from '@/stores/nostr'
|
|
import { KeyRound, Copy, Check } from 'lucide-vue-next'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'success'): void
|
|
}>()
|
|
|
|
const nostrStore = useNostrStore()
|
|
const privkey = ref('')
|
|
const error = ref('')
|
|
const isLoading = ref(false)
|
|
const copied = ref(false)
|
|
const showRecoveryMessage = ref(false)
|
|
|
|
const login = async () => {
|
|
if (!privkey.value) return
|
|
|
|
try {
|
|
isLoading.value = true
|
|
await nostrStore.login(privkey.value)
|
|
emit('success')
|
|
} catch (err) {
|
|
console.error('Login failed:', err)
|
|
error.value = 'Invalid private key'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const generateKey = () => {
|
|
privkey.value = window.NostrTools.generatePrivateKey()
|
|
showRecoveryMessage.value = true
|
|
}
|
|
|
|
const copyKey = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(privkey.value)
|
|
copied.value = true
|
|
setTimeout(() => {
|
|
copied.value = false
|
|
}, 2000)
|
|
} catch (err) {
|
|
console.error('Failed to copy:', err)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.animate-in {
|
|
animation: animate-in 0.5s cubic-bezier(0.21, 1.02, 0.73, 1);
|
|
}
|
|
|
|
@keyframes animate-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px) scale(0.98);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
/* Improved focus styles */
|
|
:focus-visible {
|
|
outline: 2px solid #cba6f7;
|
|
outline-offset: 2px;
|
|
transition: outline-offset 0.2s ease;
|
|
}
|
|
|
|
/* Enhanced button hover states */
|
|
button:not(:disabled):hover {
|
|
transform: translateY(-1px) scale(1.01);
|
|
}
|
|
|
|
button:not(:disabled):active {
|
|
transform: translateY(0) scale(0.99);
|
|
}
|
|
|
|
/* Smooth transitions */
|
|
* {
|
|
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter;
|
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
transition-duration: 300ms;
|
|
}
|
|
|
|
/* Glass morphism effects */
|
|
.backdrop-blur-sm {
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
}
|
|
</style> |