webapp/src/components/Login.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>