chore: Refactor project setup and remove Nostr-specific components

- Remove Nostr-related components (ConnectionStatus, Login)
- Update package.json with performance and analysis tools
- Configure Vite for improved build optimization
- Simplify i18n locales by removing Atitlán-specific content
- Add .cursorrules file with development guidelines
- Update Navbar and Footer to be more generic
This commit is contained in:
padreug 2025-03-09 13:05:33 +01:00
parent 3d356225cd
commit c1f32c0ea6
9 changed files with 114 additions and 561 deletions

View file

@ -1,120 +0,0 @@
<script setup lang="ts">
import { useNostrStore } from '@/stores/nostr'
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'
const nostrStore = useNostrStore()
const router = useRouter()
const route = useRoute()
const status = computed(() => nostrStore.connectionStatus)
const hasUnread = computed(() => nostrStore.hasUnreadMessages)
const isOnSupportChat = computed(() => route.path === '/support')
// Compute theme-specific classes
const indicatorClasses = computed(() => ({
'bg-primary/20 dark:bg-primary/30': true,
'shadow-[0_0_8px_rgba(var(--primary),0.3)] dark:shadow-[0_0_12px_rgba(var(--primary),0.4)]': true
}))
const breatheClasses = computed(() => ({
'bg-primary/10 dark:bg-primary/20': true,
'shadow-[0_0_12px_rgba(var(--primary),0.2)] dark:shadow-[0_0_16px_rgba(var(--primary),0.3)]': true
}))
async function handleIndicatorClick() {
if (isOnSupportChat.value) {
return // Do nothing if already on support chat
}
if (status.value === 'connected') {
// Navigate to support chat if connected
router.push('/support')
} else {
// Attempt to reconnect if not connected
try {
await nostrStore.init() // Use init instead of reconnectToRelays
} catch (err) {
console.error('Failed to reconnect:', err)
}
}
}
</script>
<template>
<div class="flex items-center gap-2">
<div class="relative">
<div
class="relative h-4 w-4 flex items-center justify-center cursor-pointer hover:scale-110 transition-transform duration-200"
@click="handleIndicatorClick"
:title="isOnSupportChat
? status
: status === 'connected'
? 'Click to open support chat'
: 'Click to reconnect'"
>
<!-- Base connection dot -->
<div
class="h-2 w-2 rounded-full z-20 relative transition-colors duration-300"
:class="{
'bg-green-500 shadow-green-500/30': status === 'connected',
'bg-yellow-500 shadow-yellow-500/30': status === 'connecting',
'bg-red-500 shadow-red-500/30': status === 'disconnected'
}"
/>
<!-- Message indicator -->
<div
v-if="hasUnread"
class="absolute inset-0 message-indicator"
>
<div
class="absolute inset-0 rounded-full transition-colors duration-300"
:class="indicatorClasses"
></div>
<div
class="absolute inset-0 rounded-full transition-colors duration-300 animate-breathe"
:class="breatheClasses"
></div>
</div>
</div>
</div>
<span class="text-sm text-muted-foreground hidden md:inline">
{{ status }}
<span v-if="hasUnread" class="text-primary font-medium ml-1">
(1)
</span>
</span>
</div>
</template>
<style scoped>
.message-indicator {
transform-origin: center;
animation: gentle-pulse 2s ease-in-out infinite;
}
@keyframes gentle-pulse {
0% { transform: scale(0.95); }
50% { transform: scale(1.1); }
100% { transform: scale(0.95); }
}
.animate-breathe {
animation: breathe 2s ease-in-out infinite;
}
@keyframes breathe {
0% {
transform: scale(1);
opacity: 0.3;
}
50% {
transform: scale(1.5);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0.3;
}
}
</style>

View file

@ -1,206 +0,0 @@
<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">
{{ t('login.title') }}
</CardTitle>
<CardDescription>
{{ t('login.description') }}
</CardDescription>
</div>
<div class="space-y-4">
<div class="space-y-2">
<div class="relative">
<Input
v-model="privkey"
:type="showKey ? 'text' : 'password'"
:placeholder="t('login.placeholder')"
:class="[
{ 'border-destructive': error },
{ 'pr-24': privkey },
]"
@keyup.enter="login"
/>
<div class="absolute right-1 top-1 flex gap-1">
<Button
v-if="privkey"
variant="ghost"
size="sm"
class="h-8"
@click="toggleShowKey"
>
<Eye v-if="!showKey" class="h-4 w-4" />
<EyeOff v-else class="h-4 w-4" />
</Button>
<Button
v-if="privkey"
variant="ghost"
size="sm"
class="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">{{ t('login.buttons.copy') }}</span>
</Button>
</div>
</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>{{ t('login.recovery.message') }}</p>
</div>
<div class="flex flex-col gap-2">
<Button @click="login" :disabled="!privkey || isLoading">
<Loader2 v-if="isLoading" class="h-4 w-4 animate-spin" />
<span v-else>{{ t('login.buttons.login') }}</span>
</Button>
<Button variant="outline" @click="generateKey" :disabled="isLoading">
{{ t('login.buttons.generate') }}
</Button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useNostrStore } from '@/stores/nostr'
import { KeyRound, Copy, Check, Eye, EyeOff, Loader2 } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { CardDescription, CardTitle } from '@/components/ui/card'
import { isValidPrivateKey, formatPrivateKey } from '@/lib/nostr'
const { t } = useI18n()
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 showKey = ref(false)
const toggleShowKey = () => {
showKey.value = !showKey.value
}
const login = async () => {
if (!privkey.value || isLoading.value) return
const formattedKey = formatPrivateKey(privkey.value)
if (!isValidPrivateKey(formattedKey)) {
error.value = t('login.error')
return
}
try {
isLoading.value = true
error.value = ''
await nostrStore.login(formattedKey)
emit('success')
} catch (err) {
console.error('Login failed:', err)
error.value = err instanceof Error ? err.message : t('login.error')
} finally {
isLoading.value = false
}
}
const generateKey = () => {
if (isLoading.value) return
try {
const newKey = window.NostrTools.generatePrivateKey()
if (!isValidPrivateKey(newKey)) {
throw new Error('Generated key is invalid')
}
privkey.value = newKey
error.value = ''
showRecoveryMessage.value = true
showKey.value = false
} catch (err) {
console.error('Failed to generate key:', err)
error.value = t('login.error')
}
}
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>

View file

@ -1,27 +1,10 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { Rocket } from 'lucide-vue-next'
const { t } = useI18n()
</script>
<template>
<footer class="bg-card border-t border-border">
<div class="mx-auto max-w-7xl px-3 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center py-2 sm:py-4 gap-2 sm:gap-4">
<!-- Powered by section -->
<div class="flex items-center gap-1.5 text-xs sm:text-sm text-muted-foreground">
<span>{{ t('footer.poweredBy') }}</span>
<img src="@/assets/bitcoin.svg" alt="Bitcoin" class="w-6 h-6 sm:w-8 sm:h-8" />
</div>
<div class="flex items-center gap-4">
<a href="https://lnbits.atitlan.io/tipjar/3" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center justify-center rounded-md bg-accent/10 px-3 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm font-medium text-accent border border-accent/20 hover:bg-accent/20 hover:border-accent/40 transition-colors">
<Rocket class="mr-1.5 sm:mr-2 h-3.5 w-3.5 sm:h-4 sm:w-4" />
{{ t('footer.donate') }}
</a>
</div>
</div>
</div>
<footer>
</footer>
</template>

View file

@ -1,25 +1,19 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Menu, X, Sun, Moon, Zap, MessageSquareText, LogIn } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { useTheme } from '@/components/theme-provider'
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'
import ConnectionStatus from '@/components/ConnectionStatus.vue'
import { Button } from '@/components/ui/button'
import { Zap, Sun, Moon, Menu, X } from 'lucide-vue-next'
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: '/' },
{ name: t('nav.support'), href: '/support', icon: MessageSquareText },
{ name: t('nav.support'), href: '/support' },
])
const toggleMenu = () => {
@ -37,15 +31,6 @@ const toggleLocale = () => {
// Store the preference
localStorage.setItem('locale', newLocale)
}
const handleLogout = async () => {
// await nostrStore.logout()
router.push('/')
}
const openLogin = () => {
showLoginDialog.value = true
}
</script>
<template>
@ -70,7 +55,7 @@ const openLogin = () => {
</div>
</div>
<!-- Theme Toggle, Language, and Auth -->
<!-- Theme Toggle and Language -->
<div class="flex items-center gap-2">
<!-- Hide theme toggle on mobile -->
<Button variant="ghost" size="icon" @click="toggleTheme" class="hidden sm:inline-flex">
@ -85,18 +70,6 @@ const openLogin = () => {
{{ locale === 'en' ? '🇪🇸 ES' : '🇺🇸 EN' }}
</Button>
<!-- <ConnectionStatus v-if="nostrStore.isLoggedIn" /> -->
<ConnectionStatus v-if="true" />
<!-- <template v-if="nostrStore.isLoggedIn"> -->
<template v-if="true">
<LogoutDialog :onLogout="handleLogout" />
</template>
<Button v-else variant="ghost" size="icon" @click="openLogin"
class="text-muted-foreground hover:text-foreground">
<LogIn class="h-5 w-5" />
</Button>
<!-- Mobile menu button -->
<Button variant="ghost" size="icon" class="md:hidden" @click="toggleMenu">
<span class="sr-only">Open main menu</span>