first commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -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?
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
Problemy na które trzeba zwrócić uwagę:
|
||||||
|
|
||||||
|
1. w .env trzeba dodać poprawny basename przy publikacji i przed użyciem komendy build
|
||||||
|
|
||||||
|
2. (stare już poprawione) po użyciu komendy build w dist/index.html trzeba dodać . przed linkami do css i js
|
||||||
|
3. (stare już poprawione) trzeba się upewnić że link do zdjęć będzie poprawny
|
||||||
|
4. (stare już poprawione) po wejściu na apkę trzeba usunąć z linku /index.html żeby zadziałała z routerem
|
||||||
|
|
||||||
|
Da się dodać taką apkę do ulubionych linków w panelu idosella ALE
|
||||||
|
|
||||||
|
1. Trzeba dodać ją z nowego panelu
|
||||||
|
2. Sam link będzie działał ze starego panelu
|
||||||
|
|
||||||
|
Użyte biblioteki
|
||||||
|
|
||||||
|
1. MUI
|
||||||
|
2. React Router
|
||||||
|
3. zustand
|
||||||
|
4. react shadow
|
||||||
|
|
||||||
|
Dodać contentEditable
|
||||||
|
przepisać działanie tej listy. Może przygotować gotowy komponent od listy w zustand
|
||||||
|
|
||||||
|
Podpięcie GraphQL
|
||||||
|
|
||||||
|
Jak zrobić Preview - CodeBox
|
||||||
|
|
||||||
|
1. Potrzeba generowania kodu HTML dla preview i codebox
|
||||||
|
2. Do Preview powinno się dać dodać jakieś funkcje pozwalające zmieniać zustand
|
||||||
|
3. zmiany w codebox powinny też pozwalać na zmianę zustand'a(może lepiej najpierw generować kod w codebox a później wklejać go do preview? tylko co później z podpinaniem funkcji do np punktów żeby dało się je przesuwać myszką? i pobieranie i używanie danych z GraphQL w Preview?)
|
||||||
|
|
||||||
|
Trzy położenia punktów
|
||||||
|
za duże plusy - kwestia czcionki apliakcji pewnie
|
||||||
|
xd przez shadow roota stylowanie komponentów z MUI nie działa w preview
|
||||||
|
|
||||||
|
poprawić rerenderowanie szczególnie inputow
|
||||||
|
|
||||||
|
problem rem em w shadow root
|
||||||
29
eslint.config.js
Normal file
29
eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
14
index.html
Normal file
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Apka reactowa</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3805
package-lock.json
generated
Normal file
3805
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "photo-module",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^7.3.6",
|
||||||
|
"@mui/material": "^7.3.6",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.11.0",
|
||||||
|
"react-shadow": "^20.6.0",
|
||||||
|
"zustand": "^5.0.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/react": "^19.2.5",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"vite": "^7.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/kotek-1.jpg
Normal file
BIN
public/kotek-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
public/kotek-2.jpg
Normal file
BIN
public/kotek-2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 617 KiB |
BIN
public/logo_1_big.png
Normal file
BIN
public/logo_1_big.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
BIN
public/test_photo.png
Normal file
BIN
public/test_photo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
24
src/App.jsx
Normal file
24
src/App.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import Home from "./pages/Home";
|
||||||
|
import "./styles/index.css";
|
||||||
|
import AppLayout from "./ui/AppLayout";
|
||||||
|
import Instruction from "./pages/Instruction";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <AppLayout>
|
||||||
|
// <Home/>
|
||||||
|
// </AppLayout>
|
||||||
|
<Routes>
|
||||||
|
<Route element={<AppLayout/>}>
|
||||||
|
<Route path="/" element={<Home/>}/>
|
||||||
|
</Route>
|
||||||
|
<Route element={<AppLayout showSidebar={false}/>}>
|
||||||
|
<Route path="/instruction" element={<Instruction/>}/>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
20
src/constants/photo.js
Normal file
20
src/constants/photo.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Constants for PhotoModule
|
||||||
|
export const DIRECTIONS = ["tl", "tr", "bl", "br"]; // top-left, top-right, bottom-left, bottom-right
|
||||||
|
export const DIRECTIONS_NAMES = {
|
||||||
|
tl: "Góra Lewo",
|
||||||
|
tr: "Góra Prawo",
|
||||||
|
bl: "Dół Lewo",
|
||||||
|
br: "Dół Prawo",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_POINT = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
id: 0,
|
||||||
|
direction: DIRECTIONS[0],
|
||||||
|
}; // Default point structure
|
||||||
|
|
||||||
|
export const URL_RADIO_DATA = [
|
||||||
|
{ value: "single", label: "Jedno Zdjęcie" },
|
||||||
|
{ value: "rwd", label: "Trzy Zdjęcia (RWD)" },
|
||||||
|
];
|
||||||
5
src/constants/rwd.js
Normal file
5
src/constants/rwd.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const BREAKPOINTS = {
|
||||||
|
mobile: 0,
|
||||||
|
tablet: 757,
|
||||||
|
desktop: 979,
|
||||||
|
};
|
||||||
16
src/features/htmlBuilder/components/CodeBox.jsx
Normal file
16
src/features/htmlBuilder/components/CodeBox.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { TextareaAutosize } from "@mui/material";
|
||||||
|
import GenericBox from "../../../ui/GenericBox";
|
||||||
|
|
||||||
|
function CodeBox() {
|
||||||
|
return (
|
||||||
|
<GenericBox variant="outer" title="Twój Kod" canCollapse={false}>
|
||||||
|
<TextareaAutosize
|
||||||
|
sx={{ width: "100%", height: "100%" }}
|
||||||
|
style={{ resize: "none" }}
|
||||||
|
minRows={5}
|
||||||
|
/>
|
||||||
|
</GenericBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeBox;
|
||||||
26
src/features/htmlBuilder/components/PreviewContent.jsx
Normal file
26
src/features/htmlBuilder/components/PreviewContent.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import root from "react-shadow";
|
||||||
|
import GenerateStyle from "../generated/generateStyle";
|
||||||
|
import GeneratePreview from "../generated/GeneratePreview";
|
||||||
|
import PreviewStickyContainer from "./PreviewStickyContainer";
|
||||||
|
import PreviewRWDTabs from "./PreviewRWDTabs";
|
||||||
|
|
||||||
|
function PreviewContent() {
|
||||||
|
return (
|
||||||
|
<PreviewStickyContainer>
|
||||||
|
<PreviewRWDTabs />
|
||||||
|
<root.div>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
:host{
|
||||||
|
font-size: 10px
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<GenerateStyle />
|
||||||
|
<GeneratePreview />
|
||||||
|
</root.div>
|
||||||
|
</PreviewStickyContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewContent;
|
||||||
33
src/features/htmlBuilder/components/PreviewRWDTabs.jsx
Normal file
33
src/features/htmlBuilder/components/PreviewRWDTabs.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
import { Button } from "@mui/material";
|
||||||
|
import { BREAKPOINTS } from "../../../constants/rwd";
|
||||||
|
import { useSharedState } from "../../../store/useSharedState";
|
||||||
|
|
||||||
|
const StyledPreviewTabsContainer = styled("div")({
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(3, 1fr)",
|
||||||
|
gap: "0.1rem",
|
||||||
|
// backgroundColor: "#060606",
|
||||||
|
});
|
||||||
|
|
||||||
|
function PreviewRWDTabs() {
|
||||||
|
const currentPreviewMode = useSharedState((state) => state.previewMode);
|
||||||
|
const setPreviewMode = useSharedState((state) => state.setPreviewMode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledPreviewTabsContainer>
|
||||||
|
{currentPreviewMode === "single" ? null : Object.keys(BREAKPOINTS).map((key) => (
|
||||||
|
<Button
|
||||||
|
key={key}
|
||||||
|
variant="contained"
|
||||||
|
disabled={currentPreviewMode === key}
|
||||||
|
onClick={() => setPreviewMode(key)}
|
||||||
|
>
|
||||||
|
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</StyledPreviewTabsContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewRWDTabs;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
function PreviewStickyContainer({ children }) {
|
||||||
|
return (
|
||||||
|
<div style={{ height: "100%", padding: "4rem 2rem" }}>
|
||||||
|
<div style={{ position: "sticky", top: "10px" }}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewStickyContainer;
|
||||||
72
src/features/htmlBuilder/generated/GeneratePreview.jsx
Normal file
72
src/features/htmlBuilder/generated/GeneratePreview.jsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useSharedState } from "../../../store/useSharedState";
|
||||||
|
import GeneratePreviewImage from "./GeneratePreviewImage";
|
||||||
|
import GeneratePreviewPoints from "./GeneratePreviewPoints";
|
||||||
|
|
||||||
|
function GeneratePreview({ preview = true }) {
|
||||||
|
const previewMode = useSharedState((state) => state.previewMode); //
|
||||||
|
const urls = useSharedState((state) => state.urls);
|
||||||
|
|
||||||
|
if (preview && urls[previewMode] === "") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="idm_picture__module">
|
||||||
|
{<GeneratePreviewImage preview={preview} urls={urls} />}
|
||||||
|
{<GeneratePreviewPoints preview={preview} />}
|
||||||
|
{/*
|
||||||
|
<div className="idm_picture__overlay">
|
||||||
|
<div
|
||||||
|
className="idm_picture__product"
|
||||||
|
style={{
|
||||||
|
"--photo-prod-point-desktop-top": "50%",
|
||||||
|
"--photo-prod-point-desktop-left": "60%",
|
||||||
|
"--photo-prod-point-tablet-top": "50%",
|
||||||
|
"--photo-prod-point-tablet-left": "60%",
|
||||||
|
"--photo-prod-point-mobile-top": "50%",
|
||||||
|
"--photo-prod-point-mobile-left": "60%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="idm_picture__product_point"
|
||||||
|
aria-describedby="prod_id_199974"
|
||||||
|
aria-label="Product Info"
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="product_info"
|
||||||
|
id="prod_id_199974"
|
||||||
|
data-id="199974"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="idm_picture__product"
|
||||||
|
style={{
|
||||||
|
"--photo-prod-point-desktop-top": "30%",
|
||||||
|
"--photo-prod-point-desktop-left": "20%",
|
||||||
|
"--photo-prod-point-tablet-top": "30%",
|
||||||
|
"--photo-prod-point-tablet-left": "20%",
|
||||||
|
"--photo-prod-point-mobile-top": "30%",
|
||||||
|
"--photo-prod-point-mobile-left": "20%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="idm_picture__product_point"
|
||||||
|
aria-describedby="prod_id_196974"
|
||||||
|
aria-label="Product Info"
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="product_info"
|
||||||
|
id="prod_id_196974"
|
||||||
|
data-id="196974"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeneratePreview;
|
||||||
53
src/features/htmlBuilder/generated/GeneratePreviewImage.jsx
Normal file
53
src/features/htmlBuilder/generated/GeneratePreviewImage.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useSharedState } from "../../../store/useSharedState";
|
||||||
|
|
||||||
|
function GeneratePreviewImage({ preview, urls }) {
|
||||||
|
const imagePrefix =
|
||||||
|
import.meta.env.MODE === "development"
|
||||||
|
? import.meta.env.VITE_PUBLIC_URL
|
||||||
|
: "";
|
||||||
|
const previewMode = useSharedState((state) => state.previewMode);
|
||||||
|
const urlRadioPoint = useSharedState((state) => state.urlRadioPoint);
|
||||||
|
const photoAlt = useSharedState((state) => state.photoAlt);
|
||||||
|
|
||||||
|
if (preview)
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={`${imagePrefix}/${urls[previewMode]}`}
|
||||||
|
alt={photoAlt || ""}
|
||||||
|
className="idm_picture__img"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{urlRadioPoint === "rwd" ? (
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
srcSet={`${imagePrefix}/${urls.desktop}`}
|
||||||
|
media="(min-width: 979px)"
|
||||||
|
/>
|
||||||
|
<source
|
||||||
|
srcSet={`${imagePrefix}/${urls.tablet}`}
|
||||||
|
media="(min-width: 757px)"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={`${imagePrefix}/${urls.mobile}`}
|
||||||
|
alt={photoAlt || ""}
|
||||||
|
className="idm_picture__img"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={`${imagePrefix}/${urls.single}`}
|
||||||
|
alt={photoAlt || ""}
|
||||||
|
className="idm_picture__img"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeneratePreviewImage;
|
||||||
23
src/features/htmlBuilder/generated/GeneratePreviewPoints.jsx
Normal file
23
src/features/htmlBuilder/generated/GeneratePreviewPoints.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useSharedState } from "../../../store/useSharedState";
|
||||||
|
import GeneratePreviewSinglePoint from "./GeneratePreviewSinglePoint";
|
||||||
|
|
||||||
|
function GeneratePreviewPoints({ preview }) {
|
||||||
|
const pointsLength = useSharedState((state) => state.points.length);
|
||||||
|
const containerRef = useRef();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="idm_picture__overlay" ref={containerRef}>
|
||||||
|
{[...Array(pointsLength)].map((_, index) => (
|
||||||
|
<GeneratePreviewSinglePoint
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
preview={preview}
|
||||||
|
containerRef={containerRef}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeneratePreviewPoints;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useSharedState } from "../../../store/useSharedState";
|
||||||
|
// Problemy - unikalne id elementu pod SEO
|
||||||
|
//
|
||||||
|
|
||||||
|
function GeneratePreviewSinglePoint({ index, preview, containerRef }) {
|
||||||
|
const { x, y, id, direction } = useSharedState(
|
||||||
|
(state) => state.points[index]
|
||||||
|
);
|
||||||
|
const setPoint = useSharedState((state) => state.setSinglePoint); // you need a setter in your store
|
||||||
|
|
||||||
|
const onPointerDown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const container = containerRef.current;
|
||||||
|
console.log(containerRef);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startY = e.clientY;
|
||||||
|
const initialX = x;
|
||||||
|
const initialY = y;
|
||||||
|
|
||||||
|
const onPointerMove = (moveEvent) => {
|
||||||
|
const deltaX = ((moveEvent.clientX - startX) / rect.width) * 100;
|
||||||
|
const deltaY = ((moveEvent.clientY - startY) / rect.height) * 100;
|
||||||
|
|
||||||
|
// clamp between 0% and 100%
|
||||||
|
const newX = Math.min(100, Math.max(0, initialX + deltaX));
|
||||||
|
const newY = Math.min(100, Math.max(0, initialY + deltaY));
|
||||||
|
|
||||||
|
setPoint(index, { x: newX, y: newY });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
window.removeEventListener("pointermove", onPointerMove);
|
||||||
|
window.removeEventListener("pointerup", onPointerUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", onPointerMove);
|
||||||
|
window.addEventListener("pointerup", onPointerUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="idm_picture__product"
|
||||||
|
style={{
|
||||||
|
"--photo-prod-point-desktop-top": `${y}%`,
|
||||||
|
"--photo-prod-point-desktop-left": `${x}%`,
|
||||||
|
// "--photo-prod-point-tablet-top": "50%",
|
||||||
|
// "--photo-prod-point-tablet-left": "60%",
|
||||||
|
// "--photo-prod-point-mobile-top": "50%",
|
||||||
|
// "--photo-prod-point-mobile-left": "60%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="idm_picture__product_point"
|
||||||
|
// aria-describedby="prod_id_199974"
|
||||||
|
aria-label="Product Info"
|
||||||
|
tabIndex="-1"
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="product_info"
|
||||||
|
// id="prod_id_199974"
|
||||||
|
data-id={id}
|
||||||
|
>
|
||||||
|
{preview && (
|
||||||
|
<>
|
||||||
|
<a className="product_name" href="#">
|
||||||
|
Produkt {index + 1}
|
||||||
|
</a>
|
||||||
|
<span className="product_price">XX,XX zł</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeneratePreviewSinglePoint;
|
||||||
183
src/features/htmlBuilder/generated/GenerateStyle.jsx
Normal file
183
src/features/htmlBuilder/generated/GenerateStyle.jsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
function GenerateStyle() {
|
||||||
|
return (
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
.idm_picture__module{
|
||||||
|
--photo-prod-box-bg: #fff;
|
||||||
|
--photo-prod-box-text: #111;
|
||||||
|
--photo-prod-point-shadow: rgba(255,255,255,.5);
|
||||||
|
|
||||||
|
--photo-prod-box-pad-top: 1em;
|
||||||
|
--photo-prod-box-pad-left: 2em;
|
||||||
|
|
||||||
|
--photo-prod-box-width: 30em;
|
||||||
|
--photo-prod-point-size: 24px;
|
||||||
|
|
||||||
|
--photo-prod-box-radius: 20px;
|
||||||
|
--photo-prod-box-radius-rb: 0px 20px 20px 20px;
|
||||||
|
--photo-prod-box-radius-lb: 20px 0px 20px 20px;
|
||||||
|
--photo-prod-box-radius-lu: 20px 20px 0px 20px;
|
||||||
|
--photo-prod-box-radius-ru: 20px 20px 20px 0px;
|
||||||
|
|
||||||
|
|
||||||
|
--photo-prod-point-desktop-top: 0%;
|
||||||
|
--photo-prod-point-desktop-left: 0%;
|
||||||
|
--photo-prod-point-tablet-top: 0%;
|
||||||
|
--photo-prod-point-tablet-left: 0%;
|
||||||
|
--photo-prod-point-mobile-top: 0%;
|
||||||
|
--photo-prod-point-mobile-left: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.idm_picture__module{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.idm_picture__product{
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.idm_picture__img{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.idm_picture__overlay{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
PRODUCT POINT ( + )
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.idm_picture__product_point{
|
||||||
|
position: relative;
|
||||||
|
width: var(--photo-prod-point-size);
|
||||||
|
height: var(--photo-prod-point-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background: var(--photo-prod-box-bg);
|
||||||
|
color: var(--photo-prod-box-text);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulsating halo */
|
||||||
|
.idm_picture__product_point::before{
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--photo-prod-point-shadow);
|
||||||
|
|
||||||
|
animation: idmPulse 1.5s ease-in-out infinite;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: stop pulse on hover */
|
||||||
|
.idm_picture__product:hover .idm_picture__product_point:before{
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus accessibility */
|
||||||
|
.idm_picture__product_point:focus-visible{
|
||||||
|
outline: 2px solid #000;
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
PULSE ANIMATION
|
||||||
|
========================= */
|
||||||
|
@keyframes idmPulse{
|
||||||
|
0%{
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 0px 0px var(--photo-prod-point-shadow);
|
||||||
|
}
|
||||||
|
70%{
|
||||||
|
box-shadow: 0 0 5px 10px var(--photo-prod-point-shadow);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100%{
|
||||||
|
box-shadow: 0 0 5px 10px var(--photo-prod-point-shadow);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product_info{
|
||||||
|
background: var(--photo-prod-box-bg);
|
||||||
|
/* display: flex; */
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--photo-prod-box-pad-top) var(--photo-prod-box-pad-left);
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-left: var(--photo-prod-box-pad-left);
|
||||||
|
margin-top: var(--photo-prod-box-pad-top);
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
max-width: var(--photo-prod-box-width);
|
||||||
|
border-radius: var(--photo-prod-box-radius-rb);
|
||||||
|
/* opacity: 0;
|
||||||
|
transition: opacity 0.3s; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.product_name{
|
||||||
|
font-size: 1.6em;
|
||||||
|
color: var(--photo-prod-box-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product_price{
|
||||||
|
font-size: 1.6em;
|
||||||
|
color: var(--photo-prod-box-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.idm_picture__product:hover .product_info{
|
||||||
|
display: flex;
|
||||||
|
animation: idmShowUp 0.3s ease-in-out;
|
||||||
|
/* opacity: 1; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes idmShowUp{
|
||||||
|
from{
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.idm_picture__product{
|
||||||
|
top: var(--photo-prod-point-mobile-top);
|
||||||
|
left: var(--photo-prod-point-mobile-left);
|
||||||
|
}
|
||||||
|
@media(min-width: 757px){
|
||||||
|
.idm_picture__product{
|
||||||
|
top: var(--photo-prod-point-tablet-top);
|
||||||
|
left: var(--photo-prod-point-tablet-left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media(min-width: 979px){
|
||||||
|
.idm_picture__product{
|
||||||
|
top: var(--photo-prod-point-desktop-top);
|
||||||
|
left: var(--photo-prod-point-desktop-left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.idm_picture__product_point{
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenerateStyle;
|
||||||
28
src/features/photoModule/PhotoModule.jsx
Normal file
28
src/features/photoModule/PhotoModule.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Divider } from "@mui/material";
|
||||||
|
import GenericBox from "../../ui/GenericBox";
|
||||||
|
import AddElementButton from "../../ui/AddElementButton";
|
||||||
|
import { useSharedState } from "../../store/useSharedState";
|
||||||
|
import PhotoPoints from "./PhotoPoints";
|
||||||
|
import PhotoUrl from "./PhotoUrl";
|
||||||
|
import { DEFAULT_POINT } from "./../../constants/photo";
|
||||||
|
|
||||||
|
function PhotoModule() {
|
||||||
|
// zustand state
|
||||||
|
const addSinglePoint = useSharedState((state) => state.addSinglePoint);
|
||||||
|
|
||||||
|
// 3. Dodawanie/usuwanie punktów
|
||||||
|
return (
|
||||||
|
<GenericBox variant="outer" title="Zdjęcie z punktami" canCollapse={true}>
|
||||||
|
<PhotoUrl />
|
||||||
|
<Divider />
|
||||||
|
<PhotoPoints />
|
||||||
|
<Divider />
|
||||||
|
<AddElementButton
|
||||||
|
onClick={() => addSinglePoint(DEFAULT_POINT)}
|
||||||
|
name="punkt"
|
||||||
|
/>
|
||||||
|
</GenericBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhotoModule;
|
||||||
17
src/features/photoModule/PhotoPoints.jsx
Normal file
17
src/features/photoModule/PhotoPoints.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useSharedState } from "../../store/useSharedState";
|
||||||
|
import GenericBox from "../../ui/GenericBox";
|
||||||
|
import PhotoSinglePoint from "./PhotoSinglePoint";
|
||||||
|
|
||||||
|
function PhotoPoints() {
|
||||||
|
const pointsLength = useSharedState((state) => state.points.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericBox variant="inner" title="Punkty">
|
||||||
|
{[...Array(pointsLength || 0)].map((_, index) => (
|
||||||
|
<PhotoSinglePoint key={index} index={index} />
|
||||||
|
))}
|
||||||
|
</GenericBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhotoPoints;
|
||||||
71
src/features/photoModule/PhotoSinglePoint.jsx
Normal file
71
src/features/photoModule/PhotoSinglePoint.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { MenuItem, Select, FormControl, InputLabel } from "@mui/material";
|
||||||
|
import InputField from "../../ui/InputField";
|
||||||
|
import { DIRECTIONS, DIRECTIONS_NAMES } from "./../../constants/photo";
|
||||||
|
import GenericBox from "../../ui/GenericBox";
|
||||||
|
import { useSharedState } from "../../store/useSharedState";
|
||||||
|
|
||||||
|
function PhotoSinglePoint({ index }) {
|
||||||
|
const setSinglePoint = useSharedState((state) => state.setSinglePoint);
|
||||||
|
const removeSinglePoint = useSharedState((state) => state.removeSinglePoint);
|
||||||
|
const point = useSharedState((state) => state.points[index]);
|
||||||
|
if (!point) return null;
|
||||||
|
const { x, y, direction, id } = point;
|
||||||
|
|
||||||
|
const handleChangeNumber = ({ e, name, min = 0, max = 100 }) => {
|
||||||
|
let newValue = Number(e.target.value);
|
||||||
|
|
||||||
|
if (newValue < min) newValue = min;
|
||||||
|
if (newValue > max) newValue = max;
|
||||||
|
|
||||||
|
setSinglePoint(index, { [name]: newValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericBox
|
||||||
|
variant="inner"
|
||||||
|
sx={{ gridTemplateColumns: "repeat(2, 1fr)" }}
|
||||||
|
title={`${index + 1}`}
|
||||||
|
removeFn={() => removeSinglePoint(index)}
|
||||||
|
>
|
||||||
|
<InputField
|
||||||
|
id={`point-${index}-id`}
|
||||||
|
type="text"
|
||||||
|
name="id produktu"
|
||||||
|
value={id}
|
||||||
|
sx={{ gridColumn: "span 2" }}
|
||||||
|
onChange={(e) => setSinglePoint(index, { id: e.target.value })}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
id={`point-${index}-x`}
|
||||||
|
type="number"
|
||||||
|
name="Położenie x"
|
||||||
|
value={x}
|
||||||
|
onChange={(e) => handleChangeNumber({ e, name: "x", min: 0, max: 100 })}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
id={`point-${index}-y`}
|
||||||
|
type="number"
|
||||||
|
name="Położenie y"
|
||||||
|
value={y}
|
||||||
|
onChange={(e) => handleChangeNumber({ e, name: "y", min: 0, max: 100 })}
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth variant="outlined" sx={{ gridColumn: "span 2" }}>
|
||||||
|
<InputLabel id="direction-label">Kierunek</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="direction-label"
|
||||||
|
value={direction}
|
||||||
|
label="Kierunek"
|
||||||
|
onChange={(e) => setSinglePoint(index, { direction: e.target.value })}
|
||||||
|
>
|
||||||
|
{DIRECTIONS.map((dir) => (
|
||||||
|
<MenuItem key={dir} value={dir}>
|
||||||
|
{DIRECTIONS_NAMES[dir]}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</GenericBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhotoSinglePoint;
|
||||||
75
src/features/photoModule/PhotoUrl.jsx
Normal file
75
src/features/photoModule/PhotoUrl.jsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Divider } from "@mui/material";
|
||||||
|
import { useSharedState } from "../../store/useSharedState";
|
||||||
|
import GenericBox from "../../ui/GenericBox";
|
||||||
|
import GenericRadioGroup from "../../ui/GenericRadioGroup";
|
||||||
|
import InputField from "../../ui/InputField";
|
||||||
|
import { URL_RADIO_DATA } from "./../../constants/photo";
|
||||||
|
|
||||||
|
function PhotoUrl() {
|
||||||
|
const urlRadioPoint = useSharedState((state) => state.urlRadioPoint);
|
||||||
|
const setUrlRadioPoint = useSharedState((state) => state.setUrlRadioPoint);
|
||||||
|
|
||||||
|
const urlSingle = useSharedState((state) => state.urls.single);
|
||||||
|
const urlDesktop = useSharedState((state) => state.urls.desktop);
|
||||||
|
const urlTablet = useSharedState((state) => state.urls.tablet);
|
||||||
|
const urlMobile = useSharedState((state) => state.urls.mobile);
|
||||||
|
const setUrl = useSharedState((state) => state.setUrl);
|
||||||
|
|
||||||
|
const photoAlt = useSharedState((state) => state.photoAlt);
|
||||||
|
const setPhotoAlt = useSharedState((state) => state.setPhotoAlt);
|
||||||
|
|
||||||
|
const setPreviewMode = useSharedState((state) => state.setPreviewMode);
|
||||||
|
|
||||||
|
const handleUrlRadioChange = (event) => {
|
||||||
|
setUrlRadioPoint(event.target.value);
|
||||||
|
setPreviewMode(event.target.value === "rwd" ? "desktop" : "single");
|
||||||
|
};
|
||||||
|
const handleChangeURL = ({ event, type }) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setUrl(type, event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericBox variant="inner" title="Zdjęcie">
|
||||||
|
<GenericRadioGroup
|
||||||
|
radioData={URL_RADIO_DATA}
|
||||||
|
value={urlRadioPoint}
|
||||||
|
onChange={handleUrlRadioChange}
|
||||||
|
direction="row"
|
||||||
|
/>
|
||||||
|
{urlRadioPoint === "rwd" ? (
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
name="URL zdjęcia desktop"
|
||||||
|
onChange={(e) => handleChangeURL({ event: e, type: "desktop" })}
|
||||||
|
value={urlDesktop}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="URL zdjęcia tablet"
|
||||||
|
onChange={(e) => handleChangeURL({ event: e, type: "tablet" })}
|
||||||
|
value={urlTablet}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
name="URL zdjęcia mobile"
|
||||||
|
onChange={(e) => handleChangeURL({ event: e, type: "mobile" })}
|
||||||
|
value={urlMobile}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<InputField
|
||||||
|
name="URL zdjęcia"
|
||||||
|
onChange={(e) => handleChangeURL({ event: e, type: "single" })}
|
||||||
|
value={urlSingle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
|
<InputField
|
||||||
|
name="Alt zdjęcia"
|
||||||
|
onChange={(e) => setPhotoAlt(e.target.value)}
|
||||||
|
value={photoAlt}
|
||||||
|
/>
|
||||||
|
</GenericBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhotoUrl;
|
||||||
28
src/main.jsx
Normal file
28
src/main.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import { ThemeProvider, CssBaseline } from '@mui/material';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import theme from './styles/theme';
|
||||||
|
// import ThemeCssVars from './styles/ThemeCssVars';
|
||||||
|
|
||||||
|
|
||||||
|
const BASENAME = import.meta.env.VITE_PUBLIC_URL || "/";
|
||||||
|
|
||||||
|
const path = window.location.pathname;
|
||||||
|
if (path.endsWith('index.html')) {
|
||||||
|
window.location.replace(BASENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter basename={BASENAME}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline/>
|
||||||
|
{/* <ThemeCssVars/> */}
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
20
src/pages/Home.jsx
Normal file
20
src/pages/Home.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
import CodeBox from "./../features/htmlBuilder/components/CodeBox";
|
||||||
|
import PhotoModule from "./../features/photoModule/PhotoModule";
|
||||||
|
|
||||||
|
const StyledAppContainer = styled("div")({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "2rem",
|
||||||
|
});
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
return (
|
||||||
|
<StyledAppContainer>
|
||||||
|
<PhotoModule />
|
||||||
|
<CodeBox />
|
||||||
|
</StyledAppContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
9
src/pages/Instruction.jsx
Normal file
9
src/pages/Instruction.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
function Instruction() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Tu jest instrukcja
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Instruction
|
||||||
65
src/store/useSharedState.js
Normal file
65
src/store/useSharedState.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { DEFAULT_POINT, URL_RADIO_DATA } from "./../constants/photo";
|
||||||
|
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
const [urlRadio, setUrlRadio] = useState(URL_RADIO_DATA[0].value);
|
||||||
|
const [points, setPoints] = useState([
|
||||||
|
{
|
||||||
|
x: 80,
|
||||||
|
y: 80,
|
||||||
|
id: 75667,
|
||||||
|
direction: "tl",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
url: {
|
||||||
|
desktop: "",
|
||||||
|
tablet: "",
|
||||||
|
mobile: "",
|
||||||
|
single: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// export const createBox = () => ({});
|
||||||
|
|
||||||
|
export const useSharedState = create((set, get) => ({
|
||||||
|
//DATA
|
||||||
|
urls: { desktop: "", tablet: "", mobile: "", single: "" },
|
||||||
|
urlRadioPoint: URL_RADIO_DATA[0].value,
|
||||||
|
points: [{ ...DEFAULT_POINT }],
|
||||||
|
photoAlt: "Zdjęcie pokazowe",
|
||||||
|
previewMode: "single", // desktop, tablet, mobile
|
||||||
|
|
||||||
|
//SETTERS/UPDATERS
|
||||||
|
setUrl: (type, url) =>
|
||||||
|
set((state) => ({ urls: { ...state.urls, [type]: url } })),
|
||||||
|
|
||||||
|
setPhotoAlt: (alt) => set({ photoAlt: alt }),
|
||||||
|
setUrlRadioPoint: (value) => set({ urlRadioPoint: value }),
|
||||||
|
setPoints: (points) => set({ points }),
|
||||||
|
setPointsLength: (length) => set({ pointsLength: length }),
|
||||||
|
setPreviewMode: (mode) => set({ previewMode: mode }),
|
||||||
|
|
||||||
|
// Update a single point by ID
|
||||||
|
setSinglePoint: (id, newData) =>
|
||||||
|
set((state) => ({
|
||||||
|
points: state.points.map((p, index) =>
|
||||||
|
index === id ? { ...p, ...newData } : p
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Add a point AND increment pointsLength
|
||||||
|
addSinglePoint: (point) =>
|
||||||
|
set((state) => ({
|
||||||
|
points: [...state.points, point],
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Remove a point AND decrement pointsLength
|
||||||
|
removeSinglePoint: (id) =>
|
||||||
|
set((state) => ({
|
||||||
|
points: state.points.filter((p, index) => index !== id),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
1
src/styles/index.css
Normal file
1
src/styles/index.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
109
src/styles/theme.js
Normal file
109
src/styles/theme.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: '#C7FC70',
|
||||||
|
light: '#F3F9EA',
|
||||||
|
dark: '#0B4430',
|
||||||
|
contrastText: '#060606',
|
||||||
|
},
|
||||||
|
secondary:{
|
||||||
|
main: "#A7B2BA",
|
||||||
|
dark: '#545454',
|
||||||
|
light: '#F2F2F2',
|
||||||
|
contrastText: '#060606',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
main: '#d32f2f',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: '#eee',
|
||||||
|
paper: "#fff",
|
||||||
|
header: "#060606",
|
||||||
|
},
|
||||||
|
text:{
|
||||||
|
primary: "#060606"
|
||||||
|
},
|
||||||
|
divider: '#060606',
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
h1: {
|
||||||
|
fontSize: '2.5rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 1.3,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
subtitle1: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
subtitle2: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiInputLabel: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
'&.Mui-focused': {
|
||||||
|
color: theme.palette.primary.dark,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
props: { variant: 'contained', color: 'primary' },
|
||||||
|
style: {
|
||||||
|
'&:hover': {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
||||||
13
src/ui/AddElementButton.jsx
Normal file
13
src/ui/AddElementButton.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||||
|
import { Button } from "@mui/material"
|
||||||
|
|
||||||
|
function AddElement({onClick, name}) {
|
||||||
|
return (
|
||||||
|
<Button size="large" variant="contained" onClick={onClick} sx={{textTransform: "none", display: "flex", alignItems: "center", gap: "0.5rem",}}>
|
||||||
|
<span>Dodaj {name}</span>
|
||||||
|
<AddCircleOutlineIcon fontSize='small'/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddElement
|
||||||
34
src/ui/AppLayout.jsx
Normal file
34
src/ui/AppLayout.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { styled } from "@mui/material";
|
||||||
|
import Header from "./Header";
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import SideBar from "./SideBar";
|
||||||
|
|
||||||
|
const StyledAppLayout = styled("div")(({ showSidebar }) => ({
|
||||||
|
display: "grid",
|
||||||
|
height: "100vh",
|
||||||
|
gridTemplateColumns: showSidebar ? "5fr 3fr" : "1fr",
|
||||||
|
gridTemplateRows: "auto 1fr",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMain = styled("main")({
|
||||||
|
padding: "4rem",
|
||||||
|
margin: "0 auto",
|
||||||
|
maxWidth: "1200px",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
function AppLayout({ showSidebar = true }) {
|
||||||
|
return (
|
||||||
|
<StyledAppLayout showSidebar={showSidebar}>
|
||||||
|
<Header sx={{ gridColumn: "1/-1", gridRow: "1/2" }} />
|
||||||
|
<StyledMain>
|
||||||
|
<Outlet />
|
||||||
|
{/* {children} */}
|
||||||
|
</StyledMain>
|
||||||
|
{showSidebar && <SideBar />}
|
||||||
|
</StyledAppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
60
src/ui/GenericBox.jsx
Normal file
60
src/ui/GenericBox.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Box, styled, Typography } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import HideShowButton from "./HideShowButton";
|
||||||
|
import RemoveButton from "./RemoveButton";
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(({ theme, variant }) => ({
|
||||||
|
position: "relative",
|
||||||
|
padding: variant === "outer" ? "2rem 2rem 3rem 2rem" : "1rem",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor:
|
||||||
|
variant === "outer" ? theme.palette.background.paper : "transparent",
|
||||||
|
border: variant === "inner" ? `1px solid ${theme.palette.divider}` : "none",
|
||||||
|
borderRadius: variant === "inner" ? "8px" : "0",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTitle = styled(Typography)(({ variant }) => ({
|
||||||
|
marginBottom: "1rem",
|
||||||
|
marginTop: variant === "inner" ? "0.5rem" : "0",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledContent = styled("div")({
|
||||||
|
display: "grid",
|
||||||
|
gap: "2rem",
|
||||||
|
});
|
||||||
|
|
||||||
|
function GenericBox({
|
||||||
|
children,
|
||||||
|
sx,
|
||||||
|
title,
|
||||||
|
variant = "outer",
|
||||||
|
canCollapse = true,
|
||||||
|
removeFn = false,
|
||||||
|
}) {
|
||||||
|
const [show, setShow] = useState(true);
|
||||||
|
|
||||||
|
const TitleComponent = variant === "outer" ? "h2" : "h3";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledBox component="section" sx={sx} variant={variant}>
|
||||||
|
{title && (
|
||||||
|
<StyledTitle variant={TitleComponent}>
|
||||||
|
<span>{title}</span>
|
||||||
|
{canCollapse && <HideShowButton setShow={setShow} show={show} />}
|
||||||
|
{removeFn && <RemoveButton removeFn={removeFn} />}
|
||||||
|
</StyledTitle>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<StyledContent
|
||||||
|
style={{ display: canCollapse && !show ? "none" : "grid" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StyledContent>
|
||||||
|
</StyledBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenericBox;
|
||||||
45
src/ui/GenericRadioGroup.jsx
Normal file
45
src/ui/GenericRadioGroup.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormLabel,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
/*
|
||||||
|
radioData = [
|
||||||
|
{
|
||||||
|
value: "",
|
||||||
|
label: ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
function GenericRadioGroup({
|
||||||
|
radioData,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
direction = "column",
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>{label}</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
row={direction === "row"}
|
||||||
|
onChange={onChange}
|
||||||
|
defaultValue={radioData[0].value}
|
||||||
|
>
|
||||||
|
{radioData.map((item) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={item.value}
|
||||||
|
value={item.value}
|
||||||
|
control={<Radio />}
|
||||||
|
label={item.label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenericRadioGroup;
|
||||||
24
src/ui/Header.jsx
Normal file
24
src/ui/Header.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { styled } from "@mui/material"
|
||||||
|
import Logo from "./Logo"
|
||||||
|
import Nav from "./Nav"
|
||||||
|
|
||||||
|
|
||||||
|
const StyledHeader = styled("header")(({ theme }) => ({
|
||||||
|
padding: "0 2rem",
|
||||||
|
backgroundColor: theme.palette.background.header,
|
||||||
|
display: "flex",
|
||||||
|
gap: "1rem",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}))
|
||||||
|
|
||||||
|
function Header({sx}) {
|
||||||
|
return (
|
||||||
|
<StyledHeader sx={sx}>
|
||||||
|
<Logo/>
|
||||||
|
<Nav/>
|
||||||
|
</StyledHeader>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header
|
||||||
13
src/ui/HideShowButton.jsx
Normal file
13
src/ui/HideShowButton.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import PlainButton from "./PlainButton";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||||
|
|
||||||
|
function HideShowButton({ setShow, show }) {
|
||||||
|
return (
|
||||||
|
<PlainButton onClick={() => setShow(!show)} sx={{ paddingLeft: "3rem" }}>
|
||||||
|
{show ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
|
</PlainButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HideShowButton;
|
||||||
27
src/ui/InputField.jsx
Normal file
27
src/ui/InputField.jsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// import styled from "@emotion/styled"
|
||||||
|
import { FormControl, Input, InputLabel } from "@mui/material";
|
||||||
|
|
||||||
|
// const StyledIdmInputField = styled("div")({
|
||||||
|
// display: "flex",
|
||||||
|
// flexDirection: "column",
|
||||||
|
// gap: "1rem"
|
||||||
|
// })
|
||||||
|
|
||||||
|
function InputField({ id, name, sx, ...inputProps }) {
|
||||||
|
return (
|
||||||
|
<FormControl sx={sx}>
|
||||||
|
{name && <InputLabel htmlFor={id}>{name}</InputLabel>}
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
color: "text.primary",
|
||||||
|
backgroundColor: "background.default",
|
||||||
|
}}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InputField;
|
||||||
11
src/ui/Logo.jsx
Normal file
11
src/ui/Logo.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useHref } from "react-router-dom";
|
||||||
|
|
||||||
|
function Logo() {
|
||||||
|
const href = useHref("/logo_1_big.png");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img src={href} width="250" alt="IdoMods logo"/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logo
|
||||||
54
src/ui/Nav.jsx
Normal file
54
src/ui/Nav.jsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Button, styled } from "@mui/material";
|
||||||
|
import { NavLink, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
const StyledNav = styled("nav")({
|
||||||
|
display: "flex",
|
||||||
|
gap: "1rem",
|
||||||
|
padding: "1rem 2rem",
|
||||||
|
alignItems: "center"
|
||||||
|
});
|
||||||
|
|
||||||
|
// optional: button wrapper
|
||||||
|
const NavButton = styled(Button)(({ theme, active }) => ({
|
||||||
|
backgroundColor: active ? theme.palette.primary.light : theme.palette.primary.main,
|
||||||
|
color: theme.palette.primary.contrastText,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: active ? theme.palette.primary.light : theme.palette.primary.dark,
|
||||||
|
},
|
||||||
|
"&[aria-current='page']": {
|
||||||
|
backgroundColor: theme.palette.primary.dark,
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
textDecoration: "underline",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function Nav() {
|
||||||
|
const location = useLocation(); // gives current pathname
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ to: '/', label: 'Apka' },
|
||||||
|
{ to: '/instruction', label: 'Instrukcja' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledNav>
|
||||||
|
{links.map(({ to, label }) => {
|
||||||
|
const isActive = location.pathname === to; // check if current route
|
||||||
|
return (
|
||||||
|
<NavButton
|
||||||
|
key={to}
|
||||||
|
component={NavLink}
|
||||||
|
to={to}
|
||||||
|
active={isActive ? 1 : 0} // pass to styled
|
||||||
|
variant="contained"
|
||||||
|
disabled = {isActive}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</NavButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StyledNav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Nav;
|
||||||
23
src/ui/PlainButton.jsx
Normal file
23
src/ui/PlainButton.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Button, styled } from "@mui/material";
|
||||||
|
|
||||||
|
const StyledPlainButton = styled(Button)({
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
textTransform: "none",
|
||||||
|
color: "inherit",
|
||||||
|
font: "inherit",
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
background: "none",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function PlainButton({children, onClick, sx}) {
|
||||||
|
return (
|
||||||
|
<StyledPlainButton onClick={onClick} sx={sx}>
|
||||||
|
{children}
|
||||||
|
</StyledPlainButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/ui/RemoveButton.jsx
Normal file
27
src/ui/RemoveButton.jsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import PlainButton from "./PlainButton";
|
||||||
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
|
|
||||||
|
function RemoveButton({ removeFn }) {
|
||||||
|
return (
|
||||||
|
<PlainButton
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "0.3rem",
|
||||||
|
right: "0.3rem",
|
||||||
|
transform: "translate(50%, -50%)",
|
||||||
|
backgroundColor: "background.paper",
|
||||||
|
color: "error.main",
|
||||||
|
borderRadius: "50%",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "background.paper", // keeps it same on hover
|
||||||
|
boxShadow: "2px -2px 3px 1px rgba(0,0,0,0.2)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={removeFn}
|
||||||
|
>
|
||||||
|
<ClearIcon />
|
||||||
|
</PlainButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoveButton;
|
||||||
14
src/ui/SideBar.jsx
Normal file
14
src/ui/SideBar.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { styled } from "@mui/material";
|
||||||
|
import PreviewContent from "./../features/htmlBuilder/components/PreviewContent";
|
||||||
|
|
||||||
|
const StyledAside = styled("aside")({});
|
||||||
|
|
||||||
|
function SideBar() {
|
||||||
|
return (
|
||||||
|
<StyledAside>
|
||||||
|
<PreviewContent />
|
||||||
|
</StyledAside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SideBar;
|
||||||
14
vite.config.js
Normal file
14
vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default ({ mode }) => {
|
||||||
|
// load .env files manually
|
||||||
|
const env = loadEnv(mode, process.cwd(), '') // '' = no prefix filtering
|
||||||
|
const base = env.VITE_PUBLIC_URL || '/'
|
||||||
|
|
||||||
|
return defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user