extract Login out of Support.vue as its own dialog; add copy button and reminder to save key
This commit is contained in:
parent
8b3f1aa14b
commit
ed1b4cb22a
3 changed files with 126 additions and 81 deletions
|
|
@ -1,99 +1,123 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-center min-h-screen p-4 bg-gradient-to-b from-[#1e1e2e] to-[#181825]">
|
<div class="flex flex-col space-y-6">
|
||||||
<Card class="w-full max-w-md animate-in slide-in-from-bottom duration-500 bg-[#181825] border-[#313244] shadow-2xl">
|
<div class="flex justify-center">
|
||||||
<CardHeader class="space-y-6 pb-8">
|
<div class="relative group">
|
||||||
<div class="flex justify-center">
|
<div
|
||||||
<div class="relative group">
|
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 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>
|
</div>
|
||||||
<div class="text-center space-y-2.5">
|
<div
|
||||||
<CardTitle class="text-2xl font-bold bg-gradient-to-r from-[#cba6f7] to-[#89b4fa] inline-block text-transparent bg-clip-text">
|
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">
|
||||||
Nostr Customer Support
|
<KeyRound class="h-8 w-8 text-foreground group-hover:scale-110 transition duration-300" />
|
||||||
</CardTitle>
|
|
||||||
<CardDescription class="text-[#a6adc8]">
|
|
||||||
Login with your Nostr private key or generate a new one
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
</div>
|
||||||
<form @submit.prevent="handleLogin" class="space-y-6">
|
|
||||||
<div class="space-y-4">
|
<div class="text-center space-y-2.5">
|
||||||
<div class="relative group">
|
<CardTitle class="text-2xl font-bold bg-gradient-to-r from-primary to-primary/80 inline-block text-transparent bg-clip-text">
|
||||||
<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>
|
Nostr Login
|
||||||
<Input
|
</CardTitle>
|
||||||
v-model="privkey"
|
<CardDescription>
|
||||||
type="password"
|
Login with your Nostr private key or generate a new one
|
||||||
placeholder="Enter your private key"
|
</CardDescription>
|
||||||
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="space-y-4">
|
||||||
</div>
|
<div class="space-y-2">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="relative">
|
||||||
<Button
|
<Input
|
||||||
type="submit"
|
v-model="privkey"
|
||||||
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"
|
type="password"
|
||||||
:disabled="!privkey"
|
placeholder="Enter your private key"
|
||||||
>
|
:class="[
|
||||||
<Key class="h-4 w-4 mr-2" />
|
{ 'border-destructive': error },
|
||||||
Login Securely
|
{ 'pr-24': privkey }, // Add padding when we have a value to prevent overlap with button
|
||||||
</Button>
|
]"
|
||||||
<Button
|
@keyup.enter="login"
|
||||||
type="button"
|
/>
|
||||||
variant="outline"
|
<Button
|
||||||
@click="generateNewKey"
|
v-if="privkey"
|
||||||
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"
|
variant="ghost"
|
||||||
:disabled="isGenerating"
|
size="sm"
|
||||||
>
|
class="absolute right-1 top-1 h-8"
|
||||||
<template v-if="isGenerating">
|
@click="copyKey"
|
||||||
<div class="h-4 w-4 mr-2 animate-spin rounded-full border-2 border-[#cba6f7] border-r-transparent" />
|
>
|
||||||
Generating...
|
<Check v-if="copied" class="h-4 w-4 text-green-500" />
|
||||||
</template>
|
<Copy v-else class="h-4 w-4" />
|
||||||
<template v-else>
|
<span class="sr-only">Copy private key</span>
|
||||||
<ShieldCheck class="h-4 w-4 mr-2" />
|
</Button>
|
||||||
Generate New Key
|
</div>
|
||||||
</template>
|
<p v-if="error" class="text-sm text-destructive">{{ error }}</p>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
<!-- Recovery message -->
|
||||||
</CardContent>
|
<div v-if="showRecoveryMessage" class="text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg">
|
||||||
<CardFooter class="text-center text-sm text-[#6c7086] pb-6">
|
<p>
|
||||||
<p class="max-w-[280px] mx-auto">
|
Make sure to save your private key in a secure location. You can use it to recover your chat history on any device.
|
||||||
Make sure to save your private key in a secure location. You'll need it to access your chat history.
|
|
||||||
</p>
|
</p>
|
||||||
</CardFooter>
|
</div>
|
||||||
</Card>
|
|
||||||
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useNostrStore } from '@/stores/nostr'
|
import { useNostrStore } from '@/stores/nostr'
|
||||||
|
import { KeyRound, Copy, Check } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { generatePrivateKey } from '@/lib/nostr'
|
|
||||||
import { Key, KeyRound, ShieldCheck } from 'lucide-vue-next'
|
const emit = defineEmits<{
|
||||||
|
(e: 'success'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const nostrStore = useNostrStore()
|
const nostrStore = useNostrStore()
|
||||||
const privkey = ref('')
|
const privkey = ref('')
|
||||||
const isGenerating = ref(false)
|
const error = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const copied = ref(false)
|
||||||
|
const showRecoveryMessage = ref(false)
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
if (!privkey.value) return
|
||||||
|
|
||||||
const generateNewKey = async () => {
|
|
||||||
isGenerating.value = true
|
|
||||||
try {
|
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 {
|
} finally {
|
||||||
isGenerating.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const generateKey = () => {
|
||||||
if (!privkey.value) return
|
privkey.value = window.NostrTools.generatePrivateKey()
|
||||||
await nostrStore.login(privkey.value)
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,15 @@ import { useTheme } from '@/components/theme-provider'
|
||||||
import { useNostrStore } from '@/stores/nostr'
|
import { useNostrStore } from '@/stores/nostr'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import LogoutDialog from '@/components/ui/logout-dialog/LogoutDialog.vue'
|
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 { t, locale } = useI18n()
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
const nostrStore = useNostrStore()
|
const nostrStore = useNostrStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
|
const showLoginDialog = ref(false)
|
||||||
|
|
||||||
const navigation = computed(() => [
|
const navigation = computed(() => [
|
||||||
{ name: t('nav.home'), href: '/' },
|
{ name: t('nav.home'), href: '/' },
|
||||||
|
|
@ -37,8 +40,13 @@ const toggleLocale = () => {
|
||||||
localStorage.setItem('locale', newLocale)
|
localStorage.setItem('locale', newLocale)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = async () => {
|
||||||
nostrStore.logout()
|
await nostrStore.logout()
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const openLogin = () => {
|
||||||
|
showLoginDialog.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -79,7 +87,8 @@ const handleLogout = () => {
|
||||||
<template v-if="nostrStore.isLoggedIn">
|
<template v-if="nostrStore.isLoggedIn">
|
||||||
<LogoutDialog :onLogout="handleLogout" />
|
<LogoutDialog :onLogout="handleLogout" />
|
||||||
</template>
|
</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" />
|
<LogIn class="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
@ -108,4 +117,11 @@ const handleLogout = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Login Dialog -->
|
||||||
|
<Dialog v-model:open="showLoginDialog">
|
||||||
|
<DialogContent class="sm:max-w-md">
|
||||||
|
<Login @success="showLoginDialog = false" />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useNostrStore } from '@/stores/nostr'
|
import { useNostrStore } from '@/stores/nostr'
|
||||||
import SupportChat from '@/components/SupportChat.vue'
|
import SupportChat from '@/components/SupportChat.vue'
|
||||||
import Login from '@/components/Login.vue'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const nostrStore = useNostrStore()
|
const nostrStore = useNostrStore()
|
||||||
|
|
||||||
|
// Redirect to home if not logged in
|
||||||
|
if (!nostrStore.isLoggedIn) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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="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">
|
||||||
<div class="h-full animate-in fade-in-50 slide-in-from-bottom-3">
|
<div class="h-full animate-in fade-in-50 slide-in-from-bottom-3">
|
||||||
<Login v-if="!nostrStore.isLoggedIn" />
|
<SupportChat v-if="nostrStore.isLoggedIn" />
|
||||||
<SupportChat v-else />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue