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