extract Login out of Support.vue as its own dialog; add copy button and reminder to save key

This commit is contained in:
padreug 2025-02-12 01:45:40 +01:00
parent 8b3f1aa14b
commit ed1b4cb22a
3 changed files with 126 additions and 81 deletions

View file

@ -1,99 +1,123 @@
<template>
<div class="flex items-center justify-center min-h-screen p-4 bg-gradient-to-b from-[#1e1e2e] to-[#181825]">
<Card class="w-full max-w-md animate-in slide-in-from-bottom duration-500 bg-[#181825] border-[#313244] shadow-2xl">
<CardHeader class="space-y-6 pb-8">
<div class="flex justify-center">
<div class="relative group">
<div class="absolute -inset-1 bg-gradient-to-r from-[#cba6f7] to-[#89b4fa] 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-[#313244] to-[#45475a] flex items-center justify-center shadow-xl group-hover:shadow-2xl transition duration-300">
<KeyRound class="h-8 w-8 text-[#cdd6f4] group-hover:scale-110 transition duration-300" />
</div>
</div>
<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="text-center space-y-2.5">
<CardTitle class="text-2xl font-bold bg-gradient-to-r from-[#cba6f7] to-[#89b4fa] inline-block text-transparent bg-clip-text">
Nostr Customer Support
</CardTitle>
<CardDescription class="text-[#a6adc8]">
Login with your Nostr private key or generate a new one
</CardDescription>
<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>
</CardHeader>
<CardContent>
<form @submit.prevent="handleLogin" class="space-y-6">
<div class="space-y-4">
<div class="relative group">
<div class="absolute -inset-0.5 bg-gradient-to-r from-[#313244] to-[#45475a] rounded-xl opacity-75 group-hover:opacity-100 blur transition duration-300"></div>
<Input
v-model="privkey"
type="password"
placeholder="Enter your private key"
class="relative font-mono text-sm bg-[#1e1e2e] border-[#313244] text-[#cdd6f4] placeholder:text-[#6c7086] focus:ring-2 focus:ring-[#cba6f7] focus:border-[#cba6f7] transition-all duration-300 shadow-lg hover:border-[#45475a] rounded-lg h-11"
/>
</div>
</div>
<div class="flex flex-col gap-3">
<Button
type="submit"
class="w-full bg-gradient-to-r from-[#cba6f7] to-[#89b4fa] text-[#11111b] hover:brightness-110 active:brightness-90 transition-all duration-300 shadow-lg hover:shadow-xl h-11 rounded-lg font-medium"
:disabled="!privkey"
>
<Key class="h-4 w-4 mr-2" />
Login Securely
</Button>
<Button
type="button"
variant="outline"
@click="generateNewKey"
class="w-full bg-gradient-to-r from-[#313244]/90 to-[#45475a]/90 hover:brightness-110 active:brightness-90 text-[#cdd6f4] border-[#45475a]/50 transition-all duration-300 shadow-lg hover:shadow-xl h-11 rounded-lg font-medium backdrop-blur-sm"
:disabled="isGenerating"
>
<template v-if="isGenerating">
<div class="h-4 w-4 mr-2 animate-spin rounded-full border-2 border-[#cba6f7] border-r-transparent" />
Generating...
</template>
<template v-else>
<ShieldCheck class="h-4 w-4 mr-2" />
Generate New Key
</template>
</Button>
</div>
</form>
</CardContent>
<CardFooter class="text-center text-sm text-[#6c7086] pb-6">
<p class="max-w-[280px] mx-auto">
Make sure to save your private key in a secure location. You'll need it to access your chat history.
</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>
</CardFooter>
</Card>
</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, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card'
import { generatePrivateKey } from '@/lib/nostr'
import { Key, KeyRound, ShieldCheck } from 'lucide-vue-next'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
const emit = defineEmits<{
(e: 'success'): void
}>()
const nostrStore = useNostrStore()
const privkey = ref('')
const isGenerating = ref(false)
const error = ref('')
const isLoading = ref(false)
const copied = ref(false)
const showRecoveryMessage = ref(false)
const generateNewKey = async () => {
isGenerating.value = true
const login = async () => {
if (!privkey.value) return
try {
privkey.value = generatePrivateKey()
isLoading.value = true
await nostrStore.login(privkey.value)
emit('success')
} catch (err) {
console.error('Login failed:', err)
error.value = 'Invalid private key'
} finally {
isGenerating.value = false
isLoading.value = false
}
}
const handleLogin = async () => {
if (!privkey.value) return
await nostrStore.login(privkey.value)
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>

View file

@ -7,12 +7,15 @@ import { useTheme } from '@/components/theme-provider'
import { useNostrStore } from '@/stores/nostr'
import { useRouter } from 'vue-router'
import LogoutDialog from '@/components/ui/logout-dialog/LogoutDialog.vue'
import Login from '@/components/Login.vue'
import { Dialog, DialogContent } from '@/components/ui/dialog'
const { t, locale } = useI18n()
const { theme, setTheme } = useTheme()
const nostrStore = useNostrStore()
const router = useRouter()
const isOpen = ref(false)
const showLoginDialog = ref(false)
const navigation = computed(() => [
{ name: t('nav.home'), href: '/' },
@ -37,8 +40,13 @@ const toggleLocale = () => {
localStorage.setItem('locale', newLocale)
}
const handleLogout = () => {
nostrStore.logout()
const handleLogout = async () => {
await nostrStore.logout()
router.push('/')
}
const openLogin = () => {
showLoginDialog.value = true
}
</script>
@ -79,7 +87,8 @@ const handleLogout = () => {
<template v-if="nostrStore.isLoggedIn">
<LogoutDialog :onLogout="handleLogout" />
</template>
<Button v-else variant="ghost" size="icon" @click="router.push('/support')" class="text-muted-foreground hover:text-foreground">
<Button v-else variant="ghost" size="icon" @click="openLogin"
class="text-muted-foreground hover:text-foreground">
<LogIn class="h-5 w-5" />
</Button>
@ -108,4 +117,11 @@ const handleLogout = () => {
</div>
</div>
</nav>
<!-- Login Dialog -->
<Dialog v-model:open="showLoginDialog">
<DialogContent class="sm:max-w-md">
<Login @success="showLoginDialog = false" />
</DialogContent>
</Dialog>
</template>

View file

@ -1,17 +1,22 @@
<script setup lang="ts">
import { useNostrStore } from '@/stores/nostr'
import SupportChat from '@/components/SupportChat.vue'
import Login from '@/components/Login.vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const nostrStore = useNostrStore()
// Redirect to home if not logged in
if (!nostrStore.isLoggedIn) {
router.push('/')
}
</script>
<template>
<div class="container max-w-4xl mx-auto h-[calc(100vh-4rem)] py-4 px-4 sm:px-6 lg:px-8">
<div class="h-full">
<div class="h-full animate-in fade-in-50 slide-in-from-bottom-3">
<Login v-if="!nostrStore.isLoggedIn" />
<SupportChat v-else />
<SupportChat v-if="nostrStore.isLoggedIn" />
</div>
</div>
</div>