{data.title}
+00303| +00304|{data.content}
+00332|diff --git a/backend/src/main.ts b/backend/src/main.ts index 058a471..bb53919 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -8,6 +8,7 @@ async function bootstrap() { const allowedOrigins = [ process.env.FRONTEND_URL ?? 'http://localhost:5173', + process.env.PWA_URL ?? 'http://localhost:5174', process.env.STRAPI_URL ?? 'http://localhost:1337', ]; diff --git a/pwa/.env.example b/pwa/.env.example new file mode 100644 index 0000000..3f81d7c --- /dev/null +++ b/pwa/.env.example @@ -0,0 +1,13 @@ +# Frontend Environment Configuration +# Copy to .env and adjust for your setup + +# ===== DOCKER MODE (frontend in container) ===== +# Use when frontend runs in Docker container +# VITE_API_URL=http://backend:3000/api/v1 + +# ===== LOCAL MODE (frontend runs locally) ===== +# Use when frontend runs locally with npm run dev +VITE_API_URL=http://localhost:3000/api/v1 + +# ===== COMMON ===== +VITE_CMS_URL=http://localhost:1337 diff --git a/pwa/.gitignore b/pwa/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/pwa/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/pwa/Dockerfile b/pwa/Dockerfile new file mode 100644 index 0000000..06d81b9 --- /dev/null +++ b/pwa/Dockerfile @@ -0,0 +1,45 @@ +# Frontend Dockerfile for Placebo.mk TanStack React App + +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY package-lock.json* ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Create non-root user +RUN addgroup -g 1001 -S nginx && \ + adduser -S nginx -u 1001 -G nginx + +# Copy built application from builder stage +COPY --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Switch to non-root user +USER nginx + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1 + +# Expose port +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/pwa/Dockerfile.dev b/pwa/Dockerfile.dev new file mode 100644 index 0000000..caf698e --- /dev/null +++ b/pwa/Dockerfile.dev @@ -0,0 +1,28 @@ +# Frontend Development Dockerfile for Placebo.mk TanStack React App + +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies with better error handling +COPY package*.json ./ +COPY package-lock.json* ./ + +# Clear npm cache and install dependencies +RUN npm cache clean --force && \ + npm install + +# Copy source code +COPY . . + +# Fix permissions - use node user that exists in base image +RUN chown -R node:node /app + +# Switch to non-root user that exists in base image +USER node + +# Expose port +EXPOSE 5173 + +# Start development server with host flag +CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file diff --git a/pwa/README.md b/pwa/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/pwa/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/pwa/components.json b/pwa/components.json new file mode 100644 index 0000000..7a212d2 --- /dev/null +++ b/pwa/components.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "tailwind": { + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui" + } +} diff --git a/pwa/eslint.config.js b/pwa/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/pwa/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/pwa/index.html b/pwa/index.html new file mode 100644 index 0000000..cfe7eab --- /dev/null +++ b/pwa/index.html @@ -0,0 +1,19 @@ + + +
+ + + + + + + + + +No updates yet. Start by publishing your first update!
\n }\n\n return (\n{update.content}
\nNo updates yet. Start by publishing your first update!
\n );\n }\n\n return (\n{update.content}
\n\n {error instanceof Error ? error.message : 'Unknown error occurred'}\n
\n {onBack && (\n \n )}\n{liveBlog.description}
\n )}\n \n\n Control the live blog status and visibility\n
\n+00115| Unapologetically sarcastic news and commentary on local and global affairs in Macedonia. Because sometimes the truth hurts more than fiction. +00116|
+00117|+00131| Freshly brewed sarcasm on current events, politics, and everything in between. +00132|
+00133|+00145| We don't do nuance. We don't do diplomatic language. Just honest (and slightly mean) commentary. +00146|
+00147|+00160| Join thousands of readers who appreciate the finer art of Macedonian sarcasm. +00161|
+00162|Latest news and articles
+00212|+00226| {article.excerpt} +00227|
+00228| )} +00229|+00246| No articles published yet. Check back soon! +00247|
+00248|{data.content}
+00332|Breaking news and live updates
\n\n {liveBlog.description}\n
\n )}\n\n No live blogs are currently active. Check back soon!\n
\n+ {error instanceof Error ? error.message : 'Unknown error occurred'} +
+ {onBack && ( + + )} +{liveBlog.description}
+ )} + ++ Control the live blog status and visibility +
++ Once you delete a live blog, there is no going back. Please be certain. +
+ +No updates yet. Start by publishing your first update!
+ ); + } + + return ( +{update.content}
+Loading...
+{comment.content}
+ + {/* Reply button and form */} + {isAuthenticated && canReply && ( ++ Најавете се за да можете да коментирате +
+ +Вчитување коментари...
++ Сè уште нема коментари. Бидете првиот што ќе коментира! +
++ {blogError instanceof Error ? blogError.message : 'Unknown error occurred'} +
+{liveBlog.description}
+ )} ++ {connectionError} +
+ +No updates yet. Check back soon!
++ Error loading live coverage +
++ No pinned live blogs at the moment +
++ Check back later for live coverage +
++ {blog.description} +
+ )} +{latestUpdate.content}
++ Mark an article as "Hero" in the admin panel to feature it here. +
++ {article.excerpt} +
+ )} + + {article.tags && article.tags.length > 0 && ( +Обидете се повторно
+Проверете подоцна
++ {article.excerpt} +
+ )} ++ Pin live blogs from the admin panel. +
++ {liveBlog.description} +
+ )} + +Admin
+ {adminLinks.map((link) => ( + + {link.label} + + ))} ++ Управување со сите написи и live блогови +
+Активни live блогови
+Објавени написи
+Закачени live блогови
+Вкупни прегледи
+Вчитување...
++ {showArchived ? 'Нема архивирани live блогови' : 'Нема live блогови'} +
+Вчитување...
++ {showArchived ? 'Нема архивирани написи' : 'Нема написи'} +
++ {article.excerpt} +
+ )} +Facebook Shares
+Twitter Shares
+WhatsApp Shares
+Telegram Shares
+Total Shares
++ {dialogType === 'delete' + ? `Дали сте сигурни дека сакате да го избришете "${itemToDelete.title}"?` + : `Дали сте сигурни дека сакате да го архивирате "${itemToDelete.title}"?`} +
+Latest news and articles
++ {article.excerpt} +
+ )} + + ++ No articles published yet. Check back soon! +
+Share this article:
++ {isLogin + ? 'Login to comment, react, and access admin features' + : 'Join our community of sarcastic news enthusiasts'} +
++ By {isLogin ? 'logging in' : 'registering'}, you agree to our{' '} + Terms of Service{' '} + and{' '} + Privacy Policy. +
++ Need help?{' '} + + Contact support + +
+{categoryDescription}
+ )} ++ {heroArticle.excerpt} +
+ )} + + ++ {article.excerpt} +
+ )} + + ++ Нема објавени статии во оваа категорија. Проверете подоцна! +
+Breaking news and live updates
++ {liveBlog.description} +
+ )} ++ No live blogs are currently active. Check back soon! +
++ Invalid YouTube URL: {url} +
+Video failed to load
++ The YouTube video could not be loaded. Please check the URL or try again later. +
++ Непристојно сатрирични вести и коментари за локални и глобални настани во Македонија. + Затоа што понекогаш вистината боли повеќе од фикцијата. +
++ Свежо подготвена сатира за тековни настани, политика и сè помеѓу тоа. +
++ Не правиме нијанси. Не правиме дипломатски јазик. Само искрени (и малку лоши) коментари. +
++ Ажурирања во реално време за разбивачки вести со нашиот систем за live blogging. Нема одложувања, само факти. +
++ Unapologetically sarcastic news and commentary on local and global affairs in Macedonia. Because sometimes the truth hurts more than fiction. +
++ Freshly brewed sarcasm on current events, politics, and everything in between. +
++ We don't do nuance. We don't do diplomatic language. Just honest (and slightly mean) commentary. +
++ Join thousands of readers who appreciate the finer art of Macedonian sarcasm. +
+Latest news and articles
++ {article.excerpt} +
+ )} ++ No articles published yet. Check back soon! +
+{data.content}
+