punkty rwd

This commit is contained in:
Pawel33359
2026-01-02 14:39:16 +01:00
parent fa01cd97e3
commit df964a8088
16 changed files with 415 additions and 178 deletions

View File

@@ -17,6 +17,7 @@ Użyte biblioteki
2. React Router
3. zustand
4. react shadow
5. react-hot-toast
Dodać contentEditable
przepisać działanie tej listy. Może przygotować gotowy komponent od listy w zustand
@@ -29,10 +30,8 @@ Jak zrobić Preview - 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
Trzy położenia punktów(+ wycentrowanie położenia boxa produktowego)
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

30
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@mui/material": "^7.3.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.11.0",
"react-shadow": "^20.6.0",
"zustand": "^5.0.9"
@@ -2267,7 +2268,8 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.3",
@@ -2724,6 +2726,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/goober": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -3302,6 +3313,23 @@
"react": "^19.2.3"
}
},
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",

View File

@@ -16,6 +16,7 @@
"@mui/material": "^7.3.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.11.0",
"react-shadow": "^20.6.0",
"zustand": "^5.0.9"

View File

@@ -3,13 +3,12 @@ import Home from "./pages/Home";
import "./styles/index.css";
import AppLayout from "./ui/AppLayout";
import Instruction from "./pages/Instruction";
import { Toaster } from "react-hot-toast";
function App() {
return (
// <AppLayout>
// <Home/>
// </AppLayout>
<>
<Toaster />
<Routes>
<Route element={<AppLayout />}>
<Route path="/" element={<Home />} />
@@ -18,7 +17,8 @@ function App() {
<Route path="/instruction" element={<Instruction />} />
</Route>
</Routes>
)
</>
);
}
export default App
export default App;

View File

@@ -1,17 +1,29 @@
import { BREAKPOINTS } from "./rwd";
// 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",
// top-left, top-right, bottom-left, bottom-right
export const DIRECTIONS = {
"t-l": "Góra Lewo",
"t-r": "Góra Prawo",
"b-l": "Dół Lewo",
"b-r": "Dół Prawo",
};
export const DEFAULT_POINT = {
x: 0,
y: 0,
positions: {
single: { x: 0, y: 0 },
// ...Object.BREAKPOINTS
desktop: { x: 0, y: 0 },
tablet: { x: 0, y: 0 },
mobile: { x: 0, y: 0 },
},
id: 0,
direction: DIRECTIONS[0],
direction: Object.keys(DIRECTIONS)[0],
}; // Default point structure
export const URL_RADIO_DATA = [

View File

@@ -1,5 +1,5 @@
export const BREAKPOINTS = {
mobile: 0,
tablet: 757,
desktop: 979,
tablet: 757,
mobile: 0,
};

View File

@@ -1,16 +1,62 @@
import { TextareaAutosize } from "@mui/material";
import { Button, TextareaAutosize } from "@mui/material";
import GenericBox from "../../../ui/GenericBox";
import GeneratePreview from "../generated/GeneratePreview";
import { useSharedState } from "../../../store/useSharedState";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
export default function CodeBox() {
const state = useSharedState();
const hiddenRef = useRef(null);
const [code, setCode] = useState("");
// Whenever state changes, update the string
useEffect(() => {
if (hiddenRef.current) {
setCode(hiddenRef.current.innerHTML);
}
}, [state.urls, state.urlRadioPoint, state.points, state.photoAlt]); // reactive slices
const copyCode = () => {
navigator.clipboard.writeText("code");
toast.success("Skopiowano tekst!");
};
const clearCode = () => {
state.clearAll();
toast.success("Wyczyszczono kod");
};
function CodeBox() {
return (
<GenericBox variant="outer" title="Twój Kod" canCollapse={false}>
{/* Hidden live component */}
<div style={{ display: "none" }} ref={hiddenRef}>
<GeneratePreview
preview={false}
urls={state.urls}
urlRadioPoint={state.urlRadioPoint}
points={state.points}
photoAlt={state.photoAlt}
/>
</div>
<div style={{ display: "flex", gap: "1rem" }}>
<Button variant="contained" onClick={copyCode}>
Kopiuj
</Button>
<Button variant="contained" onClick={clearCode}>
Wyczyść
</Button>
<Button variant="contained">Wczytaj kod</Button>
</div>
{/* Textarea showing the HTML */}
<TextareaAutosize
sx={{ width: "100%", height: "100%" }}
style={{ resize: "none" }}
minRows={5}
value={code}
/>
</GenericBox>
);
}
export default CodeBox;

View File

@@ -2,6 +2,7 @@ import styled from "@emotion/styled";
import { Button } from "@mui/material";
import { BREAKPOINTS } from "../../../constants/rwd";
import { useSharedState } from "../../../store/useSharedState";
import { capitalizeFirstLetter } from "../../../utils/capitalizeFirstLetter";
const StyledPreviewTabsContainer = styled("div")({
display: "grid",
@@ -16,14 +17,16 @@ function PreviewRWDTabs() {
return (
<StyledPreviewTabsContainer>
{currentPreviewMode === "single" ? null : Object.keys(BREAKPOINTS).map((key) => (
{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)}
{capitalizeFirstLetter(key)}
</Button>
))}
</StyledPreviewTabsContainer>

View File

@@ -12,59 +12,6 @@ function GeneratePreview({ preview = true }) {
<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>
);
}

View File

@@ -3,22 +3,27 @@ import { useSharedState } from "../../../store/useSharedState";
//
function GeneratePreviewSinglePoint({ index, preview, containerRef }) {
const { x, y, id, direction } = useSharedState(
const { positions, id, direction } = useSharedState(
(state) => state.points[index]
);
const setPoint = useSharedState((state) => state.setSinglePoint); // you need a setter in your store
const previewMode = useSharedState((state) => state.previewMode);
const setSinglePointPosition = useSharedState(
(state) => state.setSinglePointPosition
); // you need a setter in your store
const [directionY, directionX] = direction.split("-");
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 initialX = positions[previewMode].x;
const initialY = positions[previewMode].y;
const onPointerMove = (moveEvent) => {
const deltaX = ((moveEvent.clientX - startX) / rect.width) * 100;
@@ -28,7 +33,8 @@ function GeneratePreviewSinglePoint({ index, preview, containerRef }) {
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 });
setSinglePointPosition(index, previewMode, "x", newX);
setSinglePointPosition(index, previewMode, "y", newY);
};
const onPointerUp = () => {
@@ -40,16 +46,13 @@ function GeneratePreviewSinglePoint({ index, preview, containerRef }) {
window.addEventListener("pointerup", onPointerUp);
};
if (preview)
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%",
top: `${positions[previewMode].x}%`,
left: `${positions[previewMode].y}%`,
}}
>
<button
@@ -65,6 +68,52 @@ function GeneratePreviewSinglePoint({ index, preview, containerRef }) {
className="product_info"
// id="prod_id_199974"
data-id={id}
data-direction-x={directionX}
data-direction-y={directionY}
>
{preview && (
<>
<a className="product_name" href="#">
Produkt {index + 1}
</a>
<span className="product_price">XX,XX </span>
</>
)}
</div>
</div>
);
return (
<div
className="idm_picture__product"
style={
previewMode === "single"
? { top: positions.single.y, left: positions.single.y }
: {
"--photo-prod-point-desktop-top": `${positions.desktop.y}%`,
"--photo-prod-point-desktop-left": `${positions.desktop.x}%`,
"--photo-prod-point-tablet-top": `${positions.tablet.y}%`,
"--photo-prod-point-tablet-left": `${positions.tablet.x}%`,
"--photo-prod-point-mobile-top": `${positions.mobile.y}%`,
"--photo-prod-point-mobile-left": `${positions.mobile.x}%`,
}
}
>
<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}
data-direction-x={directionX}
data-direction-y={directionY}
>
{preview && (
<>

View File

@@ -14,10 +14,10 @@ function GenerateStyle() {
--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-box-radius-br: 0px 20px 20px 20px;
--photo-prod-box-radius-bl: 20px 0px 20px 20px;
--photo-prod-box-radius-tl: 20px 20px 0px 20px;
--photo-prod-box-radius-tr: 20px 20px 20px 0px;
--photo-prod-point-desktop-top: 0%;
@@ -26,6 +26,14 @@ function GenerateStyle() {
--photo-prod-point-tablet-left: 0%;
--photo-prod-point-mobile-top: 0%;
--photo-prod-point-mobile-left: 0%;
--photo-box-offset: 1em;
// --photo-box-offset-t: calc(-1 * var(--photo-box-offset));
// --photo-box-offset-b: calc(var(--photo-prod-point-size) + var(--photo-box-offset));
// --photo-box-offset-l: calc(var(--photo-prod-point-size) + var(--photo-box-offset));
// --photo-box-offset-r: calc(-1 * var(--photo-box-offset));
}
.idm_picture__module{
@@ -38,14 +46,12 @@ function GenerateStyle() {
.idm_picture__img{
width: 100%;
}
.idm_picture__overlay{
width: 100%;
height: 100%;
position: absolute;
top: 0;
}
/* =========================
PRODUCT POINT ( + )
========================= */
@@ -119,14 +125,56 @@ PULSE ANIMATION
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; */
position: absolute;
width: max-content;
}
.product_info::before{
content: "";
position: absolute;
display: block;
width: calc(100% + var(--photo-box-offset) + var(--photo-prod-point-size));
height: calc(100% + var(--photo-box-offset) + var(--photo-prod-point-size));
}
.product_info[data-direction-y="t"]{
bottom: calc(100% + var(--photo-box-offset));
}
.product_info[data-direction-y="b"]{
top: calc(100% + var(--photo-box-offset));
}
.product_info[data-direction-x="l"]{
right: calc(100% + var(--photo-box-offset));
}
.product_info[data-direction-x="r"]{
left: calc(100% + var(--photo-box-offset));
}
.product_info[data-direction-y="t"]::before{
top: 0;
}
.product_info[data-direction-y="b"]::before{
bottom: 0;
}
.product_info[data-direction-x="l"]::before{
left: 0;
}
.product_info[data-direction-x="r"]::before{
right: 0;
}
.product_info[data-direction-y="t"][data-direction-x="l"]{
border-radius: var(--photo-prod-box-radius-tl);
}
.product_info[data-direction-y="t"][data-direction-x="r"]{
border-radius: var(--photo-prod-box-radius-tr);
}
.product_info[data-direction-y="b"][data-direction-x="l"]{
border-radius: var(--photo-prod-box-radius-bl);
}
.product_info[data-direction-y="b"][data-direction-x="r"]{
border-radius: var(--photo-prod-box-radius-br);
}
.product_name{
@@ -172,6 +220,9 @@ PULSE ANIMATION
}
}
.idm_picture__product_point{
cursor: grab;
}

View File

@@ -0,0 +1,5 @@
import { useSharedState } from "../../../store/useSharedState";
export default function useRecoverCode(code) {
const state = useSharedState();
}

View File

@@ -0,0 +1,115 @@
import styled from "@emotion/styled";
import { BREAKPOINTS } from "../../constants/rwd";
import { useSharedState } from "../../store/useSharedState";
import InputField from "../../ui/InputField";
import { capitalizeFirstLetter } from "../../utils/capitalizeFirstLetter";
const StyledPointsContainer = styled("div")({
display: "grid",
gridTemplateColumns: "100px 1fr 1fr",
gap: "0.5rem",
alignItems: "center",
});
function PhotoPointPosition({ index }) {
const setSinglePointPosition = useSharedState(
(state) => state.setSinglePointPosition
);
const urlRadioPoint = useSharedState((state) => state.urlRadioPoint);
const positions = useSharedState((state) => state.points[index].positions);
const handleChangeNumber = ({
e,
brName,
positionName,
min = 0,
max = 100,
}) => {
let newValue = Number(e.target.value);
if (newValue < min) newValue = min;
if (newValue > max) newValue = max;
setSinglePointPosition(index, brName, positionName, newValue);
};
return (
<div style={{ display: "flex", gap: "1.5rem", flexDirection: "column" }}>
{urlRadioPoint === "single" ? (
<StyledPointsContainer>
<span>Położenie:</span>
<InputField
id={`point-${index}-x`}
type="number"
name="lewo-prawo"
value={positions.single.x}
onChange={(e) =>
handleChangeNumber({
e,
brName: "single",
positionName: "x",
min: 0,
max: 100,
})
}
/>
<InputField
id={`point-${index}-y`}
type="number"
name="góra-dół"
value={positions.single.y}
onChange={(e) =>
handleChangeNumber({
e,
brName: "single",
positionName: "y",
min: 0,
max: 100,
})
}
/>
</StyledPointsContainer>
) : (
<>
{Object.keys(BREAKPOINTS).map((brPoint) => (
<StyledPointsContainer key={brPoint}>
<span>{capitalizeFirstLetter(brPoint)}:</span>
<InputField
id={`point-${index}-x`}
type="number"
name="lewo-prawo"
value={positions[brPoint].x}
onChange={(e) =>
handleChangeNumber({
e,
brName: brPoint,
positionName: "x",
min: 0,
max: 100,
})
}
/>
<InputField
id={`point-${index}-y`}
type="number"
name="góra-dół"
value={positions[brPoint].y}
onChange={(e) =>
handleChangeNumber({
e,
brName: brPoint,
positionName: "y",
min: 0,
max: 100,
})
}
/>
</StyledPointsContainer>
))}
</>
)}
</div>
);
}
export default PhotoPointPosition;

View File

@@ -1,29 +1,19 @@
import { MenuItem, Select, FormControl, InputLabel } from "@mui/material";
import InputField from "../../ui/InputField";
import { DIRECTIONS, DIRECTIONS_NAMES } from "./../../constants/photo";
import { DIRECTIONS } from "./../../constants/photo";
import GenericBox from "../../ui/GenericBox";
import { useSharedState } from "../../store/useSharedState";
import PhotoPointPosition from "./PhotoPointPosition";
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 });
};
const direction = useSharedState((state) => state.points[index].direction);
const id = useSharedState((state) => state.points[index].id);
return (
<GenericBox
variant="inner"
sx={{ gridTemplateColumns: "repeat(2, 1fr)" }}
title={`${index + 1}`}
removeFn={() => removeSinglePoint(index)}
>
@@ -32,24 +22,10 @@ function PhotoSinglePoint({ index }) {
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" }}>
<PhotoPointPosition index={index} />
<FormControl fullWidth variant="outlined">
<InputLabel id="direction-label">Kierunek</InputLabel>
<Select
labelId="direction-label"
@@ -57,9 +33,9 @@ function PhotoSinglePoint({ index }) {
label="Kierunek"
onChange={(e) => setSinglePoint(index, { direction: e.target.value })}
>
{DIRECTIONS.map((dir) => (
{Object.keys(DIRECTIONS).map((dir) => (
<MenuItem key={dir} value={dir}>
{DIRECTIONS_NAMES[dir]}
{DIRECTIONS[dir]}
</MenuItem>
))}
</Select>

View File

@@ -1,37 +1,18 @@
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
const defaultState = {
urls: { desktop: "", tablet: "", mobile: "", single: "" },
urlRadioPoint: URL_RADIO_DATA[0].value,
points: [{ ...DEFAULT_POINT }],
photoAlt: "Zdjęcie pokazowe",
previewMode: "single", // desktop, tablet, mobile
};
export const useSharedState = create((set, get) => ({
//DATA
...defaultState,
//SETTERS/UPDATERS
setUrl: (type, url) =>
@@ -51,6 +32,24 @@ export const useSharedState = create((set, get) => ({
),
})),
setSinglePointPosition: (id, brName, positionName, newValue) =>
set((state) => ({
points: state.points.map((p, index) =>
index === id
? {
...p,
positions: {
...p.positions,
[brName]: {
...p.positions[brName],
[positionName]: newValue,
},
},
}
: p
),
})),
// Add a point AND increment pointsLength
addSinglePoint: (point) =>
set((state) => ({
@@ -62,4 +61,6 @@ export const useSharedState = create((set, get) => ({
set((state) => ({
points: state.points.filter((p, index) => index !== id),
})),
clearAll: () => set(() => ({ ...defaultState })),
}));

View File

@@ -0,0 +1,4 @@
export function capitalizeFirstLetter(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
}