Skip to content

Commit e2055ce

Browse files
authored
Merge pull request #2 from codebridger/dev
Dev
2 parents 4911fa7 + a951334 commit e2055ce

21 files changed

Lines changed: 986 additions & 206 deletions

File tree

docs/ROADMAP.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,17 @@ The goal is to have a browsable, usable site where users can find and play songs
6060

6161
### 4. Discovery & Search Engine
6262
*Helping users find specific content or explore new music.*
63-
- [ ] **Search Page (`pages/discovery.vue` or `/search`)**:
63+
- [x] **Search Page (`pages/discovery.vue` or `/search`)**:
6464
- Accept query parameters (`?q=`, `?genre=`, `?key=`).
6565
- Implement a layout with Filters Sidebar (Desktop) / Drawer (Mobile) and Results Grid.
66-
- [ ] **Filtering Logic**:
66+
- [x] **Filtering Logic**:
6767
- Filter by Genre (Pop, Folklore, etc.), Key (Cm, Am...), and Rhythm.
6868
- Sort options (Newest, Most Viewed, A-Z).
69-
- [ ] **Results Grid**:
69+
- [x] **Results Grid**:
7070
- Efficient pagination or Infinite Scroll for large result sets.
7171
- Re-use `SongCard` with optimized props for list views.
7272
- "No Results" empty state with a "Request Song" CTA.
73-
- [ ] **Artists Index (`pages/artists/index.vue`)**:
73+
- [x] **Artists Index (`pages/artists/index.vue`)**:
7474
- A simple, paginated grid of all available artists, sortable by name or popularity.
7575

7676
---

end_user/components/widget/LanguageSwitcher.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Languages } from 'lucide-vue-next'
44
import type { UILanguageOption } from '~/stores/contentLanguage'
55
import { useAppConfigStore } from '~/stores/appConfig'
66
7-
const { locale } = useI18n()
87
const appConfig = useAppConfigStore()
98
109
const languages: { code: UILanguageOption; label: string; nativeLabel: string }[] = [
Lines changed: 23 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,57 @@
11
<template>
2-
<div class="language-switcher" v-if="availableLangs.length > 1">
3-
<div class="switcher-label">{{ t('song.language') || 'Language' }}:</div>
4-
<div class="switcher-buttons">
5-
<button
6-
v-for="lang in availableLangs"
7-
:key="lang"
8-
:class="{
9-
active: currentLang === lang,
10-
unavailable: !isLangAvailable(lang)
11-
}"
12-
@click="switchToLang(lang)"
13-
:title="getLangLabel(lang)"
14-
>
15-
{{ getLangCode(lang) }}
16-
</button>
17-
</div>
2+
<div v-if="availableLangs.length > 1" class="flex justify-center mb-4 language-switcher-wrapper" dir="rtl">
3+
<SegmentedControl :model-value="currentLang" :options="languageOptions" size="md"
4+
@update:model-value="switchToLang" />
185
</div>
196
</template>
207

218
<script setup lang="ts">
229
import { computed } from 'vue'
23-
import { useRoute, useRouter } from 'vue-router'
10+
import { useRouter } from 'vue-router'
2411
import { useI18n } from 'vue-i18n'
2512
import type { LanguageCode } from '~/constants/routes'
2613
import { ROUTES } from '~/constants/routes'
14+
import SegmentedControl from '~/components/base/SegmentedControl.vue'
2715
2816
const props = defineProps<{
2917
availableLangs: LanguageCode[]
3018
currentLang: LanguageCode
3119
songId: string
3220
}>()
3321
34-
const route = useRoute()
3522
const router = useRouter()
3623
const { t } = useI18n()
3724
25+
const languageOptions = computed(() => {
26+
return props.availableLangs.map((lang) => ({
27+
value: lang,
28+
label: getLangLabel(lang),
29+
}))
30+
})
31+
3832
const switchToLang = (lang: LanguageCode) => {
3933
if (!props.availableLangs.includes(lang)) return
40-
41-
const newPath = lang === 'ckb-IR'
34+
35+
const newPath = lang === 'ckb-IR'
4236
? ROUTES.TAB.DETAIL(props.songId) // Default: no lang in URL
4337
: ROUTES.TAB.DETAIL(props.songId, lang)
44-
45-
router.push(newPath)
46-
}
4738
48-
const getLangLabel = (lang: LanguageCode) => {
49-
const labels = {
50-
'ckb-IR': 'سورانی (ایران)',
51-
'ckb-Latn': 'سورانی (لاتین)',
52-
'kmr': 'کرمانجی',
53-
}
54-
return labels[lang] || lang
55-
}
56-
57-
const getLangCode = (lang: LanguageCode) => {
58-
// Show short code for UI
59-
return lang.split('-')[0].toUpperCase()
39+
router.push(newPath)
6040
}
6141
62-
const isLangAvailable = (lang: LanguageCode) => {
63-
return props.availableLangs.includes(lang)
42+
const getLangLabel = (lang: LanguageCode): string => {
43+
const translation = t(`song.languages.${lang}`)
44+
return (translation as string) || lang
6445
}
6546
</script>
6647

6748
<style scoped>
68-
.language-switcher {
69-
display: flex;
70-
align-items: center;
71-
gap: 0.5rem;
72-
padding: 0.75rem;
73-
background: var(--surface-card, #f9fafb);
74-
border-radius: 0.5rem;
75-
margin-bottom: 1rem;
76-
}
77-
78-
.switcher-label {
79-
font-weight: 500;
80-
color: var(--text-secondary, #6b7280);
49+
.language-switcher-wrapper :deep(button) {
50+
white-space: nowrap;
51+
min-width: fit-content;
8152
}
8253
83-
.switcher-buttons {
84-
display: flex;
85-
gap: 0.25rem;
86-
}
87-
88-
.switcher-buttons button {
89-
padding: 0.5rem 1rem;
90-
border: 1px solid var(--border, #e5e7eb);
91-
background: var(--surface-base, #ffffff);
92-
border-radius: 0.25rem;
93-
cursor: pointer;
94-
transition: all 0.2s;
95-
font-weight: 500;
96-
}
97-
98-
.switcher-buttons button:hover {
99-
background: var(--surface-hover, #f3f4f6);
100-
}
101-
102-
.switcher-buttons button.active {
103-
background: var(--brand-primary, #3b82f6);
104-
color: white;
105-
border-color: var(--brand-primary, #3b82f6);
106-
}
107-
108-
.switcher-buttons button.unavailable {
109-
opacity: 0.5;
110-
cursor: not-allowed;
54+
.language-switcher-wrapper :deep(span) {
55+
white-space: nowrap;
11156
}
11257
</style>
113-

end_user/composables/useBaseUrl.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Composable to get the base URL dynamically based on the request
3+
* Works for both server-side (SSR) and client-side rendering
4+
* Handles Nginx proxy headers (x-forwarded-*) and direct requests
5+
* Returns a computed ref that updates reactively
6+
*/
7+
export const useBaseUrl = () => {
8+
// Lazy initialization - only access Nuxt composables when computed is accessed
9+
return computed(() => {
10+
// Client-side: Use window.location
11+
if (import.meta.client && typeof window !== 'undefined') {
12+
return window.location.origin
13+
}
14+
15+
// Server-side: Try to get from headers
16+
if (import.meta.server) {
17+
try {
18+
// useRequestHeaders() is safer and handles context checking
19+
const headers = useRequestHeaders(['host', 'x-forwarded-host', 'x-forwarded-proto'])
20+
21+
// Check for Nginx proxy headers first
22+
const protocol = headers['x-forwarded-proto'] || 'https'
23+
const host = headers['x-forwarded-host'] ||
24+
headers['host'] ||
25+
'goranee.ir'
26+
27+
// Remove port if it's standard (80 for http, 443 for https)
28+
const hostWithoutPort = host.split(':')[0]
29+
const port = host.includes(':') ? host.split(':')[1] : null
30+
31+
// Only include port if it's non-standard
32+
let baseUrl = `${protocol}://${hostWithoutPort}`
33+
if (port && port !== '80' && port !== '443') {
34+
baseUrl = `${protocol}://${hostWithoutPort}:${port}`
35+
}
36+
37+
return baseUrl
38+
} catch (e) {
39+
// Headers not available in this context, will use fallback
40+
}
41+
}
42+
43+
// Fallback: Try runtime config
44+
try {
45+
const config = useRuntimeConfig()
46+
const envBaseUrl = config.public.baseUrl as string | undefined
47+
if (envBaseUrl) {
48+
return envBaseUrl
49+
}
50+
} catch (e) {
51+
// Runtime config not available, use default
52+
}
53+
54+
// Final fallback
55+
return 'https://goranee.ir'
56+
})
57+
}

end_user/composables/useImageUrl.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,35 @@ import { fileProvider } from '@modular-rest/client'
66
* (not internal Docker URLs like http://server:8081)
77
*/
88
export const useImageUrl = () => {
9+
// Get runtime config at composable initialization (valid Nuxt context)
10+
let ssrBaseUrl: string | undefined = undefined
11+
12+
try {
13+
if (!process.client) {
14+
// Server-side (SSR): Use ssrApiBaseUrl from runtime config
15+
// This is set at build time via NUXT_SSR_API_BASE_URL build arg in Dockerfile
16+
const config = useRuntimeConfig()
17+
const configBaseUrl = config.public.ssrApiBaseUrl
18+
19+
if (configBaseUrl && typeof configBaseUrl === 'string' && configBaseUrl.trim()) {
20+
ssrBaseUrl = configBaseUrl.trim().replace(/\/$/, '')
21+
}
22+
}
23+
} catch (e) {
24+
// Runtime config not available, will use fallback
25+
}
26+
27+
// Fallback: Try reading from process.env (for development or if runtime config isn't set)
28+
if (!ssrBaseUrl && !process.client) {
29+
const envBaseUrl = process.env.NUXT_SSR_API_BASE_URL || process.env.VITE_SSR_API_BASE_URL
30+
if (envBaseUrl && typeof envBaseUrl === 'string' && envBaseUrl.trim()) {
31+
ssrBaseUrl = envBaseUrl.trim().replace(/\/$/, '')
32+
console.warn('[useImageUrl] Using SSR base URL from process.env (fallback):', ssrBaseUrl)
33+
} else {
34+
console.error('[useImageUrl] NUXT_SSR_API_BASE_URL is not configured. Image URLs will use internal Docker URL which is not accessible to clients.')
35+
}
36+
}
37+
938
/**
1039
* Get a public-facing image URL for client-side use
1140
* During SSR, uses NUXT_SSR_API_BASE_URL to generate URLs accessible to browsers
@@ -14,30 +43,10 @@ export const useImageUrl = () => {
1443
const getImageUrl = (file: any): string | undefined => {
1544
if (!file) return undefined
1645

17-
let baseUrl: string | undefined = undefined
18-
19-
if (!process.client) {
20-
// Server-side (SSR): Use ssrApiBaseUrl from runtime config
21-
// This is set at build time via NUXT_SSR_API_BASE_URL build arg in Dockerfile
22-
// The value is baked into the build, so it's available via runtime config
23-
const config = useRuntimeConfig()
24-
const ssrBaseUrl = config.public.ssrApiBaseUrl
25-
26-
if (ssrBaseUrl && typeof ssrBaseUrl === 'string' && ssrBaseUrl.trim()) {
27-
baseUrl = ssrBaseUrl.trim().replace(/\/$/, '')
28-
} else {
29-
// Fallback: Try reading from process.env (for development or if runtime config isn't set)
30-
const envBaseUrl = process.env.NUXT_SSR_API_BASE_URL || process.env.VITE_SSR_API_BASE_URL
31-
if (envBaseUrl && typeof envBaseUrl === 'string' && envBaseUrl.trim()) {
32-
baseUrl = envBaseUrl.trim().replace(/\/$/, '')
33-
console.warn('[useImageUrl] Using SSR base URL from process.env (fallback):', baseUrl)
34-
} else {
35-
console.error('[useImageUrl] NUXT_SSR_API_BASE_URL is not configured. Image URLs will use internal Docker URL which is not accessible to clients.')
36-
}
37-
}
38-
}
46+
// Server-side: Use pre-fetched ssrBaseUrl
3947
// Client-side: baseUrl is undefined, so fileProvider will use GlobalOptions.host
4048
// which is set to window.location.origin + /api/ in the plugin
49+
const baseUrl = !process.client ? ssrBaseUrl : undefined
4150

4251
return fileProvider.getFileLink(file, baseUrl)
4352
}

0 commit comments

Comments
 (0)