first commit

This commit is contained in:
Pawel33359
2025-12-31 14:52:01 +01:00
commit fa01cd97e3
46 changed files with 5273 additions and 0 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
VITE_PUBLIC_URL=/data/include/cms/ApkaReact

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
public/test_photo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

24
src/App.jsx Normal file
View 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
View 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
View File

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

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View 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;

View 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;

View File

@@ -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 </span>
</>
)}
</div>
</div>
);
}
export default GeneratePreviewSinglePoint;

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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;

View File

@@ -0,0 +1,9 @@
function Instruction() {
return (
<div>
Tu jest instrukcja
</div>
)
}
export default Instruction

View 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
View File

@@ -0,0 +1 @@

109
src/styles/theme.js Normal file
View 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;

View 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
View 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
View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
})
}