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:
parent
3d356225cd
commit
c1f32c0ea6
9 changed files with 114 additions and 561 deletions
58
.cursorrules
Normal file
58
.cursorrules
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
You are an expert in TypeScript, Node.js, Vite, Vue.js, Vue Router, Pinia, VueUse, Shadcn-vue, and Tailwind v4, with a deep understanding of best practices and performance optimization techniques in these technologies.
|
||||
|
||||
Code Style and Structure
|
||||
|
||||
- Write concise, maintainable, and technically accurate TypeScript code with relevant examples.
|
||||
- Use functional and declarative programming patterns; avoid classes.
|
||||
- Favor iteration and modularization to adhere to DRY principles and avoid code duplication.
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
|
||||
- Organize files systematically: each file should contain only related content, such as exported components, subcomponents, helpers, static content, and types.
|
||||
|
||||
Naming Conventions
|
||||
|
||||
- Use lowercase with dashes for directories (e.g., components/auth-wizard).
|
||||
- Favor named exports for functions.
|
||||
|
||||
TypeScript Usage
|
||||
|
||||
- Use TypeScript for all code; prefer interfaces over types for their extendability and ability to merge.
|
||||
- Avoid enums; use maps instead for better type safety and flexibility.
|
||||
- Use functional components with TypeScript interfaces.
|
||||
|
||||
Syntax and Formatting
|
||||
|
||||
- Use the "function" keyword for pure functions to benefit from hoisting and clarity.
|
||||
- Always use the Vue Composition API script setup style.
|
||||
|
||||
Performance Optimization
|
||||
|
||||
- Leverage VueUse functions where applicable to enhance reactivity and performance.
|
||||
- Wrap asynchronous components in Suspense with a fallback UI.
|
||||
- Use dynamic loading for non-critical components.
|
||||
- Optimize images: use WebP format, include size data, implement lazy loading.
|
||||
- Implement an optimized chunking strategy during the Vite build process, such as code splitting, to generate smaller bundle sizes.
|
||||
|
||||
Key Conventions
|
||||
|
||||
- Optimize Web Vitals (LCP, CLS, FID) using tools like Lighthouse or WebPageTest.
|
||||
- Use the VueUse library for performance-enhancing functions.
|
||||
- Implement lazy loading for non-critical components.
|
||||
- Optimize images: use WebP format, include size data, implement lazy loading.
|
||||
- Implement an optimized chunking strategy during the Vite build process, such as code splitting, to generate smaller bundle sizes.
|
||||
|
||||
Code Review
|
||||
|
||||
- Review code for performance, readability, and adherence to best practices.
|
||||
- Ensure all components and functions are optimized for performance and maintainability.
|
||||
- Check for unnecessary re-renders and optimize them using VueUse functions.
|
||||
- Use the VueUse library for performance-enhancing functions.
|
||||
- Implement lazy loading for non-critical components.
|
||||
- Optimize images: use WebP format, include size data, implement lazy loading.
|
||||
- Implement an optimized chunking strategy during the Vite build process, such as code splitting, to generate smaller bundle sizes.
|
||||
|
||||
Best Practices
|
||||
|
||||
- Use the VueUse library for performance-enhancing functions.
|
||||
- Implement lazy loading for non-critical components.
|
||||
- Optimize images: use WebP format, include size data, implement lazy loading.
|
||||
- Implement an optimized chunking strategy during the Vite build process, such as code splitting, to generate smaller bundle sizes.
|
||||
14
package.json
14
package.json
|
|
@ -6,15 +6,17 @@
|
|||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"analyze": "vite build --mode analyze"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/components": "^12.5.0",
|
||||
"@vueuse/head": "^2.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lucide-vue-next": "^0.474.0",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"pinia": "^2.3.1",
|
||||
"radix-vue": "^1.9.13",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
|
|
@ -22,18 +24,24 @@
|
|||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.14.2",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-router": "^4.5.0",
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.0.1",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/rollup-plugin-visualizer": "^4.2.3",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sharp": "^0.33.2",
|
||||
"tailwindcss": "^4.0.1",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-image-optimizer": "^1.1.7",
|
||||
"vite-plugin-inspect": "^0.8.3",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
nav: {
|
||||
title: 'Atitlán Directory',
|
||||
title: 'Title Here',
|
||||
home: 'Home',
|
||||
directory: 'Directory',
|
||||
faq: 'FAQ',
|
||||
|
|
@ -8,92 +8,4 @@ export default {
|
|||
login: 'Login',
|
||||
logout: 'Logout'
|
||||
},
|
||||
auth: {
|
||||
logout: {
|
||||
title: 'Backup Required',
|
||||
description: 'If you haven\'t saved your private key, you will permanently lose access to this chat history. Make sure to copy and securely store your private key before logging out.',
|
||||
copyKey: 'Copy Private Key',
|
||||
copied: 'Copied!',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Logout'
|
||||
}
|
||||
},
|
||||
login: {
|
||||
title: 'Nostr Login',
|
||||
description: 'Login with your Nostr private key or generate a new one',
|
||||
placeholder: 'Enter your private key',
|
||||
error: 'Invalid private key',
|
||||
buttons: {
|
||||
login: 'Login',
|
||||
generate: 'Generate New Key',
|
||||
copy: 'Copy private key'
|
||||
},
|
||||
recovery: {
|
||||
message: 'Make sure to save your private key in a secure location. You can use it to recover your chat history on any device.'
|
||||
}
|
||||
},
|
||||
home: {
|
||||
title: 'Find Bitcoin Lightning Acceptors',
|
||||
subtitle: 'Discover local businesses, services, and venues that accept Bitcoin Lightning payments.',
|
||||
browse: 'Browse Directory',
|
||||
learnMore: 'Learn More',
|
||||
selectTown: 'Location',
|
||||
selectTownPlaceholder: 'Select your location...'
|
||||
},
|
||||
directory: {
|
||||
title: 'Lightning Payment Directory',
|
||||
subtitle: 'Find local businesses and services that accept Bitcoin Lightning payments',
|
||||
search: 'Search...',
|
||||
categories: {
|
||||
all: 'All',
|
||||
restaurant: 'Restaurants',
|
||||
lodging: 'Lodging',
|
||||
goods: 'Goods',
|
||||
services: 'Services',
|
||||
taxi: 'Taxis',
|
||||
lancha: 'Lanchas',
|
||||
other: 'Other',
|
||||
tuktuk: 'Tuk-tuk'
|
||||
},
|
||||
towns: {
|
||||
all: 'All',
|
||||
},
|
||||
contact: 'Contact',
|
||||
location: 'Location',
|
||||
viewMap: 'View on Map',
|
||||
lightning: 'Lightning Address',
|
||||
itemNotFound: 'Directory Item Not Found',
|
||||
itemNotFoundDesc: 'The directory item you are looking for could not be found.',
|
||||
backToDirectory: 'Back to Directory',
|
||||
share: 'Share',
|
||||
shareTitle: 'Share this listing',
|
||||
copyLink: 'Copy Link',
|
||||
linkCopied: 'Link copied to clipboard!',
|
||||
onlinePresence: 'Online Presence',
|
||||
},
|
||||
footer: {
|
||||
poweredBy: 'Powered by',
|
||||
donate: 'Donate',
|
||||
},
|
||||
faq: {
|
||||
title: 'Frequently Asked Questions',
|
||||
items: [
|
||||
{
|
||||
question: "What is Bitcoin Lightning?",
|
||||
answer: "Bitcoin Lightning is a layer 2 payment protocol that operates on top of the Bitcoin blockchain. It enables instant, low-cost transactions between participating nodes, making it ideal for everyday purchases."
|
||||
},
|
||||
{
|
||||
question: "How do I pay with Lightning?",
|
||||
answer: "To pay with Lightning, you'll need a Lightning-enabled Bitcoin wallet. When making a purchase, simply scan the merchant's QR code with your wallet and confirm the payment. The transaction is nearly instant!"
|
||||
},
|
||||
{
|
||||
question: "Is Lightning safe to use?",
|
||||
answer: "Yes, Lightning is secure as it inherits Bitcoin's security model. While it operates differently from base-layer Bitcoin transactions, it maintains high security standards through smart contract technology."
|
||||
},
|
||||
{
|
||||
question: "How can I list my business?",
|
||||
answer: "If you're a business accepting Lightning payments and would like to be listed in our directory, please contact us through our submission form [coming soon]."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,97 +3,9 @@ export default {
|
|||
home: 'Inicio',
|
||||
directory: 'Directorio',
|
||||
faq: 'Preguntas',
|
||||
title: 'Directorio Atitlán',
|
||||
title: 'Titulo Aqui',
|
||||
support: 'Soporte',
|
||||
login: 'Iniciar Sesión',
|
||||
logout: 'Cerrar Sesión'
|
||||
},
|
||||
home: {
|
||||
title: 'Encuentra Aceptadores de Bitcoin Lightning',
|
||||
subtitle: 'Descubre negocios locales, servicios y lugares que aceptan pagos Bitcoin Lightning.',
|
||||
browse: 'Explorar Directorio',
|
||||
learnMore: 'Más Información',
|
||||
selectTown: 'Ubicación',
|
||||
selectTownPlaceholder: 'Selecciona tu ubicación...'
|
||||
},
|
||||
directory: {
|
||||
title: 'Directorio de Pagos Lightning',
|
||||
subtitle: 'Encuentra lugares que aceptan pagos Bitcoin Lightning',
|
||||
search: 'Buscar...',
|
||||
categories: {
|
||||
all: 'Todo',
|
||||
restaurant: 'Restaurante',
|
||||
lodging: 'Hospedaje',
|
||||
goods: 'Productos',
|
||||
services: 'Servicios',
|
||||
taxi: 'Taxi',
|
||||
tuktuk: 'Tuk-tuk',
|
||||
lancha: 'Lancha',
|
||||
other: 'Otro'
|
||||
},
|
||||
towns: {
|
||||
all: 'Todos',
|
||||
},
|
||||
contact: 'Contacto',
|
||||
location: 'Ubicación',
|
||||
viewMap: 'Ver en Mapa',
|
||||
lightning: 'Dirección Lightning',
|
||||
itemNotFound: 'Elemento del Directorio No Encontrado',
|
||||
itemNotFoundDesc: 'No se pudo encontrar el elemento del directorio que estás buscando.',
|
||||
backToDirectory: 'Volver al Directorio',
|
||||
share: 'Compartir',
|
||||
shareTitle: 'Compartir este listado',
|
||||
copyLink: 'Copiar enlace',
|
||||
linkCopied: '¡Enlace copiado al portapapeles!',
|
||||
onlinePresence: 'Presencia en Línea',
|
||||
},
|
||||
footer: {
|
||||
poweredBy: 'Alimentado por',
|
||||
donate: 'Donar',
|
||||
},
|
||||
faq: {
|
||||
title: 'Preguntas Frecuentes',
|
||||
items: [
|
||||
{
|
||||
question: "¿Qué es Bitcoin Lightning?",
|
||||
answer: "Bitcoin Lightning es un protocolo de pago de capa 2 que opera sobre la blockchain de Bitcoin. Permite transacciones instantáneas y de bajo costo entre nodos participantes, haciéndolo ideal para compras cotidianas."
|
||||
},
|
||||
{
|
||||
question: "¿Cómo pago con Lightning?",
|
||||
answer: "Para pagar con Lightning, necesitarás una billetera Bitcoin habilitada para Lightning. Al realizar una compra, simplemente escanea el código QR del comerciante con tu billetera y confirma el pago. ¡La transacción es casi instantánea!"
|
||||
},
|
||||
{
|
||||
question: "¿Es seguro usar Lightning?",
|
||||
answer: "Sí, Lightning es seguro ya que hereda el modelo de seguridad de Bitcoin. Si bien opera de manera diferente a las transacciones de capa base de Bitcoin, mantiene altos estándares de seguridad a través de la tecnología de contratos inteligentes."
|
||||
},
|
||||
{
|
||||
question: "¿Cómo puedo listar mi negocio?",
|
||||
answer: "Si eres un negocio que acepta pagos Lightning y te gustaría aparecer en nuestro directorio, contáctanos a través de nuestro formulario de envío [próximamente]."
|
||||
}
|
||||
]
|
||||
},
|
||||
auth: {
|
||||
logout: {
|
||||
title: 'Respaldo Requerido',
|
||||
description: 'Si no has guardado tu llave privada, perderás permanentemente el acceso a este historial de chat. Asegúrate de copiar y almacenar de forma segura tu llave privada antes de cerrar sesión.',
|
||||
copyKey: 'Copiar Llave Privada',
|
||||
copied: '¡Copiado!',
|
||||
cancel: 'Cancelar',
|
||||
confirm: 'Cerrar Sesión'
|
||||
}
|
||||
},
|
||||
login: {
|
||||
title: 'Iniciar Sesión Nostr',
|
||||
description: 'Inicia sesión con tu llave privada Nostr o genera una nueva',
|
||||
placeholder: 'Ingresa tu llave privada',
|
||||
error: 'Llave privada inválida',
|
||||
buttons: {
|
||||
login: 'Iniciar Sesión',
|
||||
generate: 'Generar Nueva Llave',
|
||||
copy: 'Copiar llave privada'
|
||||
},
|
||||
recovery: {
|
||||
message: 'Asegúrate de guardar tu llave privada en un lugar seguro. Puedes usarla para recuperar tu historial de chat en cualquier dispositivo.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import vue from '@vitejs/plugin-vue'
|
|||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import Inspect from 'vite-plugin-inspect'
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
|
|
@ -23,9 +26,9 @@ export default defineConfig({
|
|||
},
|
||||
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
|
||||
manifest: {
|
||||
name: 'Atitlán Directory',
|
||||
short_name: 'Atitlán',
|
||||
description: 'Find Bitcoin Lightning acceptors around Lake Atitlán',
|
||||
name: 'Vue Shadcn App',
|
||||
short_name: 'Vue App',
|
||||
description: 'A Vue 3 app with Shadcn UI',
|
||||
theme_color: '#ffffff',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
|
|
@ -51,11 +54,41 @@ export default defineConfig({
|
|||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
Inspect(),
|
||||
ViteImageOptimizer({
|
||||
jpg: {
|
||||
quality: 80
|
||||
},
|
||||
png: {
|
||||
quality: 80
|
||||
},
|
||||
webp: {
|
||||
lossless: true
|
||||
}
|
||||
}),
|
||||
mode === 'analyze' && visualizer({
|
||||
open: true,
|
||||
filename: 'dist/stats.html',
|
||||
gzipSize: true,
|
||||
brotliSize: true
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'ui-vendor': ['radix-vue', '@vueuse/core'],
|
||||
'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge']
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
chunkSizeWarningLimit: 1000
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue