This commit is contained in:
Tomas Dvorak
2026-03-13 14:34:19 +01:00
parent 84a8acf944
commit 30d70a6aeb
126 changed files with 27297 additions and 29069 deletions
+3 -1
View File
@@ -51,7 +51,9 @@ func main() {
// Optional migrations for eshop-specific tables only (will be added later)
runMigrations, _ := strconv.ParseBool(os.Getenv("RUN_MIGRATIONS"))
if runMigrations {
log.Println("[eshop] RUN_MIGRATIONS is true, but no eshop-specific migrations are defined yet")
if err := database.MigrateDB(dbInstance); err != nil {
log.Fatalf("[eshop] Failed to run database migrations: %v", err)
}
}
// Initialize Gin router with a similar hardened stack as the main backend
-3
View File
@@ -15,9 +15,6 @@ COPY . .
# Build app
ENV NODE_ENV=production
ENV GENERATE_SOURCEMAP=false
ENV CI=true
ENV TSC_COMPILE_ON_ERROR=true
RUN npm run build
@@ -9,5 +9,6 @@
<body>
<noscript>Pro zobrazení e-shopu je potřeba povolit JavaScript.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+1627 -14329
View File
File diff suppressed because it is too large Load Diff
+13 -8
View File
@@ -3,10 +3,11 @@
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"start": "vite",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@chakra-ui/react": "^2.8.2",
@@ -15,18 +16,22 @@
"@stripe/react-stripe-js": "^5.4.1",
"@stripe/stripe-js": "^8.5.3",
"@tanstack/react-query": "^4.36.1",
"axios": "^1.6.2",
"axios": "^1.13.6",
"dompurify": "^3.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.5.0",
"react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"react-router-dom": "^6.30.2",
"typescript": "^4.9.5"
},
"devDependencies": {
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/react-router-dom": "^5.3.3"
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.4.1",
"jsdom": "^26.1.0",
"vite": "^6.3.5",
"vitest": "^3.1.1"
},
"browserslist": {
"production": [
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { getProduct, addToCart, EshopProductVariant } from '../services/eshopApi';
import { Box, Heading, Text, Image, Badge, HStack, VStack, Button, Select, useToast } from '@chakra-ui/react';
import { sanitizeRichHtml } from '../utils/sanitizeHtml';
const formatPrice = (cents: number, currency: string) => {
const value = cents / 100;
@@ -20,6 +21,7 @@ const ProductDetailPage: React.FC = () => {
enabled: !!slug,
});
const [variantId, setVariantId] = useState<number | undefined>(undefined);
const descriptionHtml = React.useMemo(() => sanitizeRichHtml(data?.description_html), [data?.description_html]);
if (isLoading) return <Text>Načítání produktu</Text>;
if (isError || !data) return <Text>Produkt nebyl nalezen.</Text>;
@@ -76,9 +78,9 @@ const ProductDetailPage: React.FC = () => {
<Button colorScheme="blue" onClick={handleAddToCart} maxW="260px">
Přidat do košíku
</Button>
{data.description_html && (
{descriptionHtml && (
<Box mt={4} fontSize="sm" color="gray.700">
<div dangerouslySetInnerHTML={{ __html: data.description_html }} />
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
</Box>
)}
</VStack>
+1 -1
View File
@@ -1 +1 @@
/// <reference types="react-scripts" />
/// <reference types="vite/client" />
+28
View File
@@ -0,0 +1,28 @@
import DOMPurify from 'dompurify';
const ADDITIONAL_TAGS = ['iframe'];
const ADDITIONAL_ATTRS = ['allow', 'allowfullscreen', 'class', 'rel', 'style', 'target'];
export function sanitizeRichHtml(html: string | null | undefined): string {
const sanitized = DOMPurify.sanitize(html ?? '', {
USE_PROFILES: { html: true },
ADD_TAGS: ADDITIONAL_TAGS,
ADD_ATTR: ADDITIONAL_ATTRS,
});
if (typeof window === 'undefined' || !sanitized) {
return sanitized;
}
const template = window.document.createElement('template');
template.innerHTML = sanitized;
template.content.querySelectorAll<HTMLAnchorElement>('a[target="_blank"]').forEach((anchor) => {
const rel = new Set((anchor.getAttribute('rel') ?? '').split(/\s+/).filter(Boolean));
rel.add('noopener');
rel.add('noreferrer');
anchor.setAttribute('rel', Array.from(rel).join(' '));
});
return template.innerHTML;
}
+1 -1
View File
@@ -15,7 +15,7 @@
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
"types": []
"types": ["vite/client", "vitest/globals"]
},
"include": ["src"]
}
+58
View File
@@ -0,0 +1,58 @@
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
function buildProcessEnv(mode: string, env: Record<string, string>, base: string) {
const processEnv: Record<string, string> = {
NODE_ENV: mode === 'production' ? 'production' : mode,
PUBLIC_URL: base === '/' ? '' : base.replace(/\/$/, ''),
};
for (const [key, value] of Object.entries(env)) {
if (key.startsWith('REACT_APP_')) {
processEnv[key] = value;
}
}
return processEnv;
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
const base = env.PUBLIC_URL ? (env.PUBLIC_URL.endsWith('/') ? env.PUBLIC_URL : `${env.PUBLIC_URL}/`) : '/';
const processEnv = buildProcessEnv(mode, env, base);
return {
base,
publicDir: 'public',
plugins: [react()],
define: {
'process.env': JSON.stringify(processEnv),
global: 'globalThis',
},
build: {
outDir: 'build',
assetsDir: 'static',
emptyOutDir: true,
sourcemap: false,
},
server: {
host: '0.0.0.0',
port: 3100,
proxy: {
'/api': {
target: 'http://127.0.0.1:8082',
changeOrigin: true,
secure: false,
},
},
},
preview: {
host: '0.0.0.0',
port: 4174,
},
test: {
environment: 'jsdom',
globals: true,
},
};
});