Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# FRONTEND CONFIGURATION
# =============================================================================
BASE_URL=http://localhost:3000
API_URL=http://localhost:8080/api
API_URL=http://localhost:8080

# =============================================================================
# BACKEND CONFIGURATION
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ services:
ports:
- "3000:3000"
environment:
- NUXT_PUBLIC_BASE_URL=http://localhost:3000
- NUXT_PUBLIC_API_URL=http://localhost:8080/api
- BASE_URL=http://localhost:3000
- API_URL=http://localhost:8080
depends_on:
- backend

Expand Down Expand Up @@ -78,8 +78,8 @@ docker run -d \
```bash
docker run -d \
-p 3000:3000 \
-e NUXT_PUBLIC_BASE_URL=http://localhost:3000 \
-e NUXT_PUBLIC_API_URL=http://localhost:8080/api \
-e BASE_URL=http://localhost:3000 \
-e API_URL=http://localhost:8080 \
ghcr.io/formeraapp/formera-frontend:latest
```

Expand All @@ -99,8 +99,8 @@ cd frontend && yarn install && yarn dev

| Variable | Description | Default |
|----------|-------------|---------|
| `NUXT_PUBLIC_BASE_URL` | Public URL of the frontend | `http://localhost:3000` |
| `NUXT_PUBLIC_API_URL` | Backend API URL | `http://localhost:8080/api` |
| `BASE_URL` | Public URL of the frontend | `http://localhost:3000` |
| `API_URL` | Backend base URL | `http://localhost:8080` |

### Backend

Expand Down Expand Up @@ -176,8 +176,8 @@ services:
image: ghcr.io/formeraapp/formera-frontend:latest
restart: unless-stopped
environment:
- NUXT_PUBLIC_BASE_URL=https://forms.example.com
- NUXT_PUBLIC_API_URL=https://forms.example.com/api
- BASE_URL=https://forms.example.com
- API_URL=https://forms.example.com
labels:
- "traefik.enable=true"
- "traefik.http.routers.formera.rule=Host(`forms.example.com`)"
Expand Down
6 changes: 3 additions & 3 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ func initStorage(cfg *config.Config) (storage.Storage, error) {

return s3Store, nil
default:
// Build full URL for local storage (ApiURL + LocalURL path)
apiURL := cfg.ApiURL + cfg.Storage.LocalURL
return storage.NewLocalStorage(cfg.Storage.LocalPath, apiURL)
// ApiURL is the backend base URL (e.g., http://localhost:8080)
uploadsURL := cfg.ApiURL + cfg.Storage.LocalURL
return storage.NewLocalStorage(cfg.Storage.LocalPath, uploadsURL)
}
}

Expand Down
4 changes: 2 additions & 2 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func init() {
type Config struct {
Port string
BaseURL string // Frontend URL (e.g., http://localhost:3000)
ApiURL string // Backend API URL (e.g., http://localhost:8080/api)
ApiURL string // Backend base URL (e.g., http://localhost:8080)
DBPath string
JWTSecret string
CorsOrigin string
Expand Down Expand Up @@ -100,7 +100,7 @@ func Load() *Config {

port := getEnv("PORT", "8080")
baseURL := getEnv("BASE_URL", "http://localhost:3000")
apiURL := getEnv("API_URL", "http://localhost:"+port+"/api")
apiURL := getEnv("API_URL", "http://localhost:"+port)

// CORS_ORIGIN defaults to BASE_URL if not set (same-origin deployment)
corsOrigin := getEnv("CORS_ORIGIN", "")
Expand Down
3 changes: 2 additions & 1 deletion backend/internal/handlers/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ func (h *FormHandler) Update(c *gin.Context) {
if req.Slug != nil {
slug := *req.Slug
if slug == "" {
form.Slug = ""
// When slug is cleared, use first 8 chars of ID (form accessible via ID)
form.Slug = form.ID[:8]
} else {
slug = normalizeSlug(slug)
if !isValidSlug(slug) {
Expand Down
12 changes: 12 additions & 0 deletions backend/internal/handlers/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type SetupStatusResponse struct {
LogoShowText bool `json:"logo_show_text"`
FaviconURL string `json:"favicon_url"`
LoginBackgroundURL string `json:"login_background_url"`
Language string `json:"language"`
Theme string `json:"theme"`
}

type SetupRequest struct {
Expand Down Expand Up @@ -56,6 +58,8 @@ func (h *SetupHandler) GetStatus(c *gin.Context) {
LogoShowText: settings.LogoShowText,
FaviconURL: settings.FaviconURL,
LoginBackgroundURL: settings.LoginBackgroundURL,
Language: settings.Language,
Theme: settings.Theme,
})
}

Expand Down Expand Up @@ -130,6 +134,8 @@ type UpdateSettingsRequest struct {
LogoShowText *bool `json:"logo_show_text"`
FaviconURL *string `json:"favicon_url"`
LoginBackgroundURL *string `json:"login_background_url"`
Language string `json:"language"`
Theme string `json:"theme"`
}

func (h *SetupHandler) UpdateSettings(c *gin.Context) {
Expand Down Expand Up @@ -166,6 +172,12 @@ func (h *SetupHandler) UpdateSettings(c *gin.Context) {
if req.LoginBackgroundURL != nil {
settings.LoginBackgroundURL = *req.LoginBackgroundURL
}
if req.Language != "" {
settings.Language = req.Language
}
if req.Theme != "" {
settings.Theme = req.Theme
}

database.DB.Save(&settings)

Expand Down
5 changes: 5 additions & 0 deletions backend/internal/models/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ type Form struct {

func (f *Form) BeforeCreate(tx *gorm.DB) error {
f.ID = uuid.New().String()
// Auto-generate unique slug from ID if not set
if f.Slug == "" {
// Use first 8 characters of UUID as slug (unique enough)
f.Slug = f.ID[:8]
}
if f.Status == "" {
f.Status = FormStatusDraft
}
Expand Down
9 changes: 7 additions & 2 deletions backend/internal/models/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ type Settings struct {
LogoShowText bool `json:"logo_show_text" gorm:"default:true"`
FaviconURL string `json:"favicon_url"`
LoginBackgroundURL string `json:"login_background_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Language and Theme
Language string `json:"language" gorm:"default:en"`
Theme string `json:"theme" gorm:"default:system"` // "light", "dark", or "system"
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func GetDefaultSettings() *Settings {
Expand All @@ -65,5 +68,7 @@ func GetDefaultSettings() *Settings {
LogoShowText: true,
FaviconURL: "",
LoginBackgroundURL: "",
Language: "en",
Theme: "system",
}
}
10 changes: 7 additions & 3 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app

COPY --from=builder /app/.output ./.output
COPY docker-entrypoint.sh /docker-entrypoint.sh

# Environment variables (override with NUXT_PUBLIC_BASE_URL and NUXT_PUBLIC_API_URL)
ENV NUXT_PUBLIC_BASE_URL=http://localhost:3000
ENV NUXT_PUBLIC_API_URL=http://localhost:8080/api
RUN chmod +x /docker-entrypoint.sh

# Environment variables - use simple BASE_URL and API_URL
ENV BASE_URL=http://localhost:3000
ENV API_URL=http://localhost:8080
ENV NITRO_PORT=3000

EXPOSE 3000

RUN addgroup -S app && adduser -S app -G app \
&& chown -R app:app /app
USER app
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["node", ".output/server/index.mjs"]
11 changes: 0 additions & 11 deletions frontend/app/app.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
<script lang="ts" setup>
onMounted(() => {
const theme = localStorage.getItem("theme");
if (theme) {
document.documentElement.setAttribute("data-theme", theme);
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.setAttribute("data-theme", "dark");
}
});
</script>

<template>
<div style="z-index: 9998">
<NuxtLayout>
Expand Down
3 changes: 1 addition & 2 deletions frontend/app/components/Builder/FormSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ const updateDesign = (key: string, value: unknown) => {
};

const handleSlugInput = (event: Event) => {
const input = event.target as HTMLInputElement;
emit("slugInput", input.value);
emit("slugInput", event);
};

const handleCopyLink = async () => {
Expand Down
33 changes: 0 additions & 33 deletions frontend/app/components/ThemeToggle.vue

This file was deleted.

13 changes: 7 additions & 6 deletions frontend/app/composables/useApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ export const getFileUrl = (pathOrUrl: string | undefined | null): string => {
const apiUrl = config.public.apiUrl as string;

const cleanPath = pathOrUrl.startsWith("/") ? pathOrUrl.slice(1) : pathOrUrl;
return `${apiUrl}/files/${cleanPath}`;
return `${apiUrl}/uploads/${cleanPath}`;
};

export const useApi = () => {
const config = useRuntimeConfig();
const apiUrl = config.public.apiUrl as string;
const apiBase = `${apiUrl}/api`;

const getToken = () => {
return localStorage.getItem("token");
Expand All @@ -28,7 +29,7 @@ export const useApi = () => {
...options.headers,
};

const response = await fetch(`${apiUrl}${endpoint}`, {
const response = await fetch(`${apiBase}${endpoint}`, {
...options,
headers,
});
Expand Down Expand Up @@ -112,11 +113,11 @@ export const useApi = () => {
stats: (formId: string): Promise<FormStats> => request(`/forms/${formId}/stats`),
exportCSV: (formId: string): string => {
const token = getToken();
return `${apiUrl}/forms/${formId}/export/csv?token=${token}`;
return `${apiBase}/forms/${formId}/export/csv?token=${token}`;
},
exportJSON: (formId: string): string => {
const token = getToken();
return `${apiUrl}/forms/${formId}/export/json?token=${token}`;
return `${apiBase}/forms/${formId}/export/json?token=${token}`;
},
};

Expand Down Expand Up @@ -169,7 +170,7 @@ export const useApi = () => {

let response: Response;
try {
response = await fetch(`${apiUrl}/uploads/image`, {
response = await fetch(`${apiBase}/uploads/image`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Expand All @@ -194,7 +195,7 @@ export const useApi = () => {

let response: Response;
try {
response = await fetch(`${apiUrl}/uploads/file`, {
response = await fetch(`${apiBase}/uploads/file`, {
method: "POST",
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
Expand Down
13 changes: 9 additions & 4 deletions frontend/app/composables/useAutoSave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ interface AutoSaveOptions {
delay?: number;
onSave: () => Promise<void>;
onError?: (error: unknown) => void;
onNoChanges?: () => void;
}

export const useAutoSave = (options: AutoSaveOptions) => {
const { t } = useI18n();
const { delay = 2000, onSave, onError } = options;
const { delay = 2000, onSave, onError, onNoChanges } = options;

const isSaving = ref(false);
const isDirty = ref(false);
Expand All @@ -15,8 +16,12 @@ export const useAutoSave = (options: AutoSaveOptions) => {

let saveTimeout: ReturnType<typeof setTimeout> | null = null;

const save = async () => {
if (!isDirty.value || isSaving.value) return;
const save = async (manual = false) => {
if (isSaving.value) return;
if (!isDirty.value) {
if (manual) onNoChanges?.();
return;
}

if (saveTimeout) {
clearTimeout(saveTimeout);
Expand Down Expand Up @@ -58,7 +63,7 @@ export const useAutoSave = (options: AutoSaveOptions) => {
clearTimeout(saveTimeout);
saveTimeout = null;
}
await save();
await save(true);
};

const cancel = () => {
Expand Down
2 changes: 0 additions & 2 deletions frontend/app/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
</div>

<div class="action-buttons">
<ThemeToggle />
<NuxtLink class="action-btn" :to="localePath('/settings')" :title="$t('nav.settings')">
<UISysIcon icon="fa-solid fa-gear" />
</NuxtLink>
Expand Down Expand Up @@ -103,7 +102,6 @@
</nav>

<div class="mobile-footer">
<ThemeToggle />
<button class="mobile-logout" @click="handleLogout">
<UISysIcon icon="fa-solid fa-right-from-bracket" />
<span>{{ $t("auth.logout") }}</span>
Expand Down
Loading
Loading