design setting
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-07-07 12:09:36 +03:30
parent e1b6ebff69
commit 4dd276d9e7
17 changed files with 1938 additions and 51 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 529 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 223 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 71 KiB

+39
View File
@@ -0,0 +1,39 @@
<svg width="440" height="957" viewBox="0 0 440 957" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_13761_13988" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="440" height="957">
<rect width="440" height="957" fill="black"/>
</mask>
<g mask="url(#mask0_13761_13988)">
<path d="M355.887 122.632L340.815 101.612C340.579 101.281 340.204 101.086 339.798 101.086C339.393 101.086 339.017 101.281 338.782 101.612L323.71 122.632C323.435 123.013 323.399 123.513 323.615 123.934C323.825 124.35 324.256 124.615 324.726 124.615H354.87C355.341 124.615 355.771 124.35 355.982 123.934C356.197 123.513 356.162 123.013 355.887 122.632ZM327.165 122.111L339.798 104.486L352.432 122.111H327.165Z" fill="#C3C7DD"/>
<path d="M233.404 122.632L218.327 101.612C218.097 101.281 217.717 101.086 217.311 101.086C216.91 101.086 216.53 101.281 216.294 101.612L201.223 122.632C200.947 123.013 200.912 123.513 201.127 123.934C201.343 124.35 201.768 124.615 202.239 124.615H232.383C232.853 124.615 233.284 124.35 233.499 123.934C233.715 123.513 233.675 123.013 233.404 122.632ZM204.678 122.111L217.311 104.486L229.949 122.111H204.678Z" fill="#C3C7DD"/>
<path d="M110.916 122.632L95.8445 101.612C95.6091 101.281 95.2286 101.086 94.828 101.086C94.4224 101.086 94.0419 101.281 93.8115 101.612L78.7347 122.632C78.4643 123.013 78.4242 123.513 78.6396 123.934C78.8549 124.35 79.2855 124.615 79.7562 124.615H109.9C110.366 124.615 110.796 124.35 111.011 123.934C111.227 123.513 111.192 123.013 110.916 122.632ZM82.1897 122.111L94.828 104.486L107.461 122.111H82.1897Z" fill="#C3C7DD"/>
<path d="M417.13 11.5462L402.058 -9.47924C401.823 -9.80472 401.442 -10 401.042 -10C400.636 -10 400.261 -9.80472 400.025 -9.47924L384.954 11.5462C384.678 11.9268 384.643 12.4275 384.853 12.8481C385.069 13.2637 385.499 13.5241 385.97 13.5241H416.114C416.584 13.5241 417.015 13.2637 417.225 12.8481C417.441 12.4275 417.406 11.9268 417.13 11.5462ZM388.408 11.0205L401.042 -6.60007L413.675 11.0205H388.408Z" fill="#C3C7DD"/>
<path d="M294.643 11.5462L279.571 -9.47924C279.336 -9.80472 278.96 -10 278.555 -10C278.154 -10 277.773 -9.80472 277.538 -9.47924L262.466 11.5462C262.191 11.9268 262.156 12.4275 262.371 12.8481C262.581 13.2637 263.012 13.5241 263.483 13.5241H293.626C294.097 13.5241 294.528 13.2637 294.743 12.8481C294.953 12.4275 294.918 11.9268 294.643 11.5462ZM265.921 11.0205L278.555 -6.60007L291.188 11.0205H265.921Z" fill="#C3C7DD"/>
<path d="M172.16 11.5462L157.089 -9.47924C156.853 -9.80472 156.473 -10 156.067 -10C155.667 -10 155.286 -9.80472 155.051 -9.47924L139.979 11.5462C139.708 11.9268 139.668 12.4275 139.884 12.8481C140.099 13.2637 140.53 13.5241 140.995 13.5241H171.144C171.61 13.5241 172.04 13.2637 172.256 12.8481C172.471 12.4275 172.431 11.9268 172.16 11.5462ZM143.434 11.0205L156.067 -6.60007L168.705 11.0205H143.434Z" fill="#C3C7DD"/>
<path d="M49.6728 11.5462L34.6009 -9.47924C34.3656 -9.80472 33.985 -10 33.5845 -10C33.1789 -10 32.8033 -9.80472 32.568 -9.47924L17.4961 11.5462C17.2207 11.9268 17.1807 12.4275 17.396 12.8481C17.6113 13.2637 18.0419 13.5241 18.5126 13.5241H48.6563C49.127 13.5241 49.5526 13.2637 49.7679 12.8481C49.9832 12.4275 49.9482 11.9268 49.6728 11.5462ZM20.9512 11.0205L33.5845 -6.60007L46.2178 11.0205H20.9512Z" fill="#C3C7DD"/>
<path d="M355.887 344.804L340.815 323.784C340.579 323.458 340.204 323.263 339.798 323.263C339.393 323.263 339.017 323.458 338.782 323.784L323.71 344.804C323.435 345.19 323.399 345.691 323.615 346.106C323.825 346.527 324.256 346.787 324.726 346.787H354.87C355.341 346.787 355.771 346.527 355.982 346.106C356.197 345.691 356.162 345.19 355.887 344.804ZM327.165 344.284L339.798 326.663L352.432 344.284H327.165Z" fill="#C3C7DD"/>
<path d="M233.404 344.804L218.327 323.784C218.097 323.458 217.717 323.263 217.311 323.263C216.91 323.263 216.53 323.458 216.294 323.784L201.223 344.804C200.947 345.19 200.912 345.691 201.127 346.106C201.343 346.527 201.768 346.787 202.239 346.787H232.383C232.853 346.787 233.284 346.527 233.499 346.106C233.715 345.691 233.675 345.19 233.404 344.804ZM204.678 344.284L217.311 326.663L229.949 344.284H204.678Z" fill="#C3C7DD"/>
<path d="M110.916 344.804L95.8445 323.784C95.6091 323.458 95.2286 323.263 94.828 323.263C94.4224 323.263 94.0419 323.458 93.8115 323.784L78.7347 344.804C78.4643 345.19 78.4242 345.691 78.6396 346.106C78.8549 346.527 79.2855 346.787 79.7562 346.787H109.9C110.366 346.787 110.796 346.527 111.011 346.106C111.227 345.691 111.192 345.19 110.916 344.804ZM82.1897 344.284L94.828 326.663L107.461 344.284H82.1897Z" fill="#C3C7DD"/>
<path d="M417.13 233.718L402.058 212.698C401.823 212.368 401.442 212.177 401.042 212.177C400.636 212.177 400.261 212.368 400.025 212.698L384.954 233.718C384.678 234.099 384.643 234.605 384.853 235.02C385.069 235.436 385.499 235.701 385.97 235.701H416.114C416.584 235.701 417.015 235.436 417.225 235.02C417.441 234.605 417.406 234.099 417.13 233.718ZM388.408 233.198L401.042 215.577L413.675 233.198H388.408Z" fill="#C3C7DD"/>
<path d="M294.643 233.718L279.571 212.698C279.336 212.368 278.96 212.177 278.555 212.177C278.154 212.177 277.773 212.368 277.538 212.698L262.466 233.718C262.191 234.099 262.156 234.605 262.371 235.02C262.581 235.436 263.012 235.701 263.483 235.701H293.626C294.097 235.701 294.528 235.436 294.743 235.02C294.953 234.605 294.918 234.099 294.643 233.718ZM265.921 233.198L278.555 215.577L291.188 233.198H265.921Z" fill="#C3C7DD"/>
<path d="M172.16 233.718L157.089 212.698C156.853 212.368 156.473 212.177 156.067 212.177C155.667 212.177 155.286 212.368 155.051 212.698L139.979 233.718C139.708 234.099 139.668 234.605 139.884 235.02C140.099 235.436 140.53 235.701 140.995 235.701H171.144C171.61 235.701 172.04 235.436 172.256 235.02C172.471 234.605 172.431 234.099 172.16 233.718ZM143.434 233.198L156.067 215.577L168.705 233.198H143.434Z" fill="#C3C7DD"/>
<path d="M49.6728 233.718L34.6009 212.698C34.3656 212.368 33.985 212.177 33.5845 212.177C33.1789 212.177 32.8033 212.368 32.568 212.698L17.4961 233.718C17.2207 234.099 17.1807 234.605 17.396 235.02C17.6113 235.436 18.0419 235.701 18.5126 235.701H48.6563C49.127 235.701 49.5526 235.436 49.7679 235.02C49.9832 234.605 49.9482 234.099 49.6728 233.718ZM20.9512 233.198L33.5845 215.577L46.2178 233.198H20.9512Z" fill="#C3C7DD"/>
<path d="M355.887 566.981L340.815 545.961C340.579 545.63 340.204 545.435 339.798 545.435C339.393 545.435 339.017 545.63 338.782 545.961L323.71 566.981C323.435 567.362 323.399 567.863 323.615 568.283C323.825 568.699 324.256 568.964 324.726 568.964H354.87C355.341 568.964 355.771 568.699 355.982 568.283C356.197 567.863 356.162 567.362 355.887 566.981ZM327.165 566.461L339.798 548.835L352.432 566.461H327.165Z" fill="#C3C7DD"/>
<path d="M233.404 566.981L218.327 545.961C218.097 545.63 217.717 545.435 217.311 545.435C216.91 545.435 216.53 545.63 216.294 545.961L201.223 566.981C200.947 567.362 200.912 567.863 201.127 568.283C201.343 568.699 201.768 568.964 202.239 568.964H232.383C232.853 568.964 233.284 568.699 233.499 568.283C233.715 567.863 233.675 567.362 233.404 566.981ZM204.678 566.461L217.311 548.835L229.949 566.461H204.678Z" fill="#C3C7DD"/>
<path d="M110.916 566.981L95.8445 545.961C95.6091 545.63 95.2286 545.435 94.828 545.435C94.4224 545.435 94.0419 545.63 93.8115 545.961L78.7347 566.981C78.4643 567.362 78.4242 567.863 78.6396 568.283C78.8549 568.699 79.2855 568.964 79.7562 568.964H109.9C110.366 568.964 110.796 568.699 111.011 568.283C111.227 567.863 111.192 567.362 110.916 566.981ZM82.1897 566.461L94.828 548.835L107.461 566.461H82.1897Z" fill="#C3C7DD"/>
<path d="M417.13 455.895L402.058 434.87C401.823 434.544 401.442 434.349 401.042 434.349C400.636 434.349 400.261 434.544 400.025 434.87L384.954 455.895C384.678 456.276 384.643 456.777 384.853 457.197C385.069 457.613 385.499 457.873 385.97 457.873H416.114C416.584 457.873 417.015 457.613 417.225 457.197C417.441 456.777 417.406 456.276 417.13 455.895ZM388.408 455.37L401.042 437.749L413.675 455.37H388.408Z" fill="#C3C7DD"/>
<path d="M294.643 455.895L279.571 434.87C279.336 434.544 278.96 434.349 278.555 434.349C278.154 434.349 277.773 434.544 277.538 434.87L262.466 455.895C262.191 456.276 262.156 456.777 262.371 457.197C262.581 457.613 263.012 457.873 263.483 457.873H293.626C294.097 457.873 294.528 457.613 294.743 457.197C294.953 456.777 294.918 456.276 294.643 455.895ZM265.921 455.37L278.555 437.749L291.188 455.37H265.921Z" fill="#C3C7DD"/>
<path d="M172.16 455.895L157.089 434.87C156.853 434.544 156.473 434.349 156.067 434.349C155.667 434.349 155.286 434.544 155.051 434.87L139.979 455.895C139.708 456.276 139.668 456.777 139.884 457.197C140.099 457.613 140.53 457.873 140.995 457.873H171.144C171.61 457.873 172.04 457.613 172.256 457.197C172.471 456.777 172.431 456.276 172.16 455.895ZM143.434 455.37L156.067 437.749L168.705 455.37H143.434Z" fill="#C3C7DD"/>
<path d="M49.6728 455.895L34.6009 434.87C34.3656 434.544 33.985 434.349 33.5845 434.349C33.1789 434.349 32.8033 434.544 32.568 434.87L17.4961 455.895C17.2207 456.276 17.1807 456.777 17.396 457.197C17.6113 457.613 18.0419 457.873 18.5126 457.873H48.6563C49.127 457.873 49.5526 457.613 49.7679 457.197C49.9832 456.777 49.9482 456.276 49.6728 455.895ZM20.9512 455.37L33.5845 437.749L46.2178 455.37H20.9512Z" fill="#C3C7DD"/>
<path d="M417.13 678.067L402.058 657.047C401.823 656.721 401.442 656.526 401.042 656.526C400.636 656.526 400.261 656.721 400.025 657.047L384.954 678.067C384.678 678.448 384.643 678.953 384.853 679.369C385.069 679.785 385.499 680.05 385.97 680.05H416.114C416.584 680.05 417.015 679.785 417.225 679.369C417.441 678.953 417.406 678.448 417.13 678.067ZM388.408 677.546L401.042 659.926L413.675 677.546H388.408Z" fill="#C3C7DD"/>
<path d="M294.643 678.067L279.571 657.047C279.336 656.721 278.96 656.526 278.555 656.526C278.154 656.526 277.773 656.721 277.538 657.047L262.466 678.067C262.191 678.448 262.156 678.953 262.371 679.369C262.581 679.785 263.012 680.05 263.483 680.05H293.626C294.097 680.05 294.528 679.785 294.743 679.369C294.953 678.953 294.918 678.448 294.643 678.067ZM265.921 677.546L278.555 659.926L291.188 677.546H265.921Z" fill="#C3C7DD"/>
<path d="M172.16 678.067L157.089 657.047C156.853 656.721 156.473 656.526 156.067 656.526C155.667 656.526 155.286 656.721 155.051 657.047L139.979 678.067C139.708 678.448 139.668 678.953 139.884 679.369C140.099 679.785 140.53 680.05 140.995 680.05H171.144C171.61 680.05 172.04 679.785 172.256 679.369C172.471 678.953 172.431 678.448 172.16 678.067ZM143.434 677.546L156.067 659.926L168.705 677.546H143.434Z" fill="#C3C7DD"/>
<path d="M49.6728 678.067L34.6009 657.047C34.3656 656.721 33.985 656.526 33.5845 656.526C33.1789 656.526 32.8033 656.721 32.568 657.047L17.4961 678.067C17.2207 678.448 17.1807 678.953 17.396 679.369C17.6113 679.785 18.0419 680.05 18.5126 680.05H48.6563C49.127 680.05 49.5526 679.785 49.7679 679.369C49.9832 678.953 49.9482 678.448 49.6728 678.067ZM20.9512 677.546L33.5845 659.926L46.2178 677.546H20.9512Z" fill="#C3C7DD"/>
<path d="M415.887 897.632L400.815 876.612C400.579 876.281 400.204 876.086 399.798 876.086C399.393 876.086 399.017 876.281 398.782 876.612L383.71 897.632C383.435 898.013 383.399 898.513 383.615 898.934C383.825 899.35 384.256 899.615 384.726 899.615H414.87C415.341 899.615 415.771 899.35 415.982 898.934C416.197 898.513 416.162 898.013 415.887 897.632ZM387.165 897.111L399.798 879.486L412.432 897.111H387.165Z" fill="#C3C7DD"/>
<path d="M293.404 897.632L278.327 876.612C278.097 876.281 277.717 876.086 277.311 876.086C276.91 876.086 276.53 876.281 276.294 876.612L261.223 897.632C260.947 898.013 260.912 898.513 261.127 898.934C261.343 899.35 261.768 899.615 262.239 899.615H292.383C292.853 899.615 293.284 899.35 293.499 898.934C293.715 898.513 293.675 898.013 293.404 897.632ZM264.678 897.111L277.311 879.486L289.949 897.111H264.678Z" fill="#C3C7DD"/>
<path d="M170.916 897.632L155.844 876.612C155.609 876.281 155.229 876.086 154.828 876.086C154.422 876.086 154.042 876.281 153.812 876.612L138.735 897.632C138.464 898.013 138.424 898.513 138.64 898.934C138.855 899.35 139.285 899.615 139.756 899.615H169.9C170.366 899.615 170.796 899.35 171.011 898.934C171.227 898.513 171.192 898.013 170.916 897.632ZM142.19 897.111L154.828 879.486L167.461 897.111H142.19Z" fill="#C3C7DD"/>
<path d="M48.429 897.632L33.3571 876.612C33.1218 876.281 32.7462 876.086 32.3407 876.086C31.9351 876.086 31.5595 876.281 31.3242 876.612L16.2523 897.632C15.9769 898.013 15.9419 898.513 16.1522 898.934C16.3675 899.35 16.7981 899.615 17.2688 899.615H47.4125C47.8832 899.615 48.3138 899.35 48.5241 898.934C48.7394 898.513 48.7044 898.013 48.429 897.632ZM19.7074 897.111L32.3407 879.486L44.974 897.111H19.7074Z" fill="#C3C7DD"/>
<path d="M354.643 786.546L339.571 765.521C339.336 765.195 338.96 765 338.555 765C338.154 765 337.773 765.195 337.538 765.521L322.466 786.546C322.191 786.927 322.156 787.428 322.371 787.848C322.581 788.264 323.012 788.524 323.483 788.524H353.626C354.097 788.524 354.528 788.264 354.743 787.848C354.953 787.428 354.918 786.927 354.643 786.546ZM325.921 786.02L338.555 768.4L351.188 786.02H325.921Z" fill="#C3C7DD"/>
<path d="M232.16 786.546L217.089 765.521C216.853 765.195 216.473 765 216.067 765C215.667 765 215.286 765.195 215.051 765.521L199.979 786.546C199.708 786.927 199.668 787.428 199.884 787.848C200.099 788.264 200.53 788.524 200.995 788.524H231.144C231.61 788.524 232.04 788.264 232.256 787.848C232.471 787.428 232.431 786.927 232.16 786.546ZM203.434 786.02L216.067 768.4L228.705 786.02H203.434Z" fill="#C3C7DD"/>
<path d="M109.673 786.546L94.6009 765.521C94.3656 765.195 93.985 765 93.5845 765C93.1789 765 92.8033 765.195 92.568 765.521L77.4961 786.546C77.2207 786.927 77.1807 787.428 77.396 787.848C77.6113 788.264 78.0419 788.524 78.5126 788.524H108.656C109.127 788.524 109.553 788.264 109.768 787.848C109.983 787.428 109.948 786.927 109.673 786.546ZM80.9512 786.02L93.5845 768.4L106.218 786.02H80.9512Z" fill="#C3C7DD"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 343 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 768 KiB

+70
View File
@@ -0,0 +1,70 @@
export const MENU_SITE_ORIGIN = "https://dkala.danakcorp.com";
/** Admin routes whose first segment must not be proxied to the public menu site. */
export const MENU_PREVIEW_RESERVED_SEGMENTS = new Set([
"auth",
"dashboard",
"setting",
"statistics",
"schedule",
"payment_methods",
"foods",
"orders",
"customers",
"discounts",
"announcements",
"reports",
"comments",
"roles",
"admins",
"shipment_methods",
"coupons",
"reviews",
"pagers",
"notifications",
"sliders",
"wallet",
"assets",
"menu-preview",
]);
export const isMenuPreviewProxyPath = (pathname: string) => {
if (!pathname.startsWith("/menu-preview/")) return false;
const rest = pathname.slice("/menu-preview".length);
return rest.length > 0;
};
export const isMenuSlugProxyPath = (pathname: string) => {
const [firstSegment] = pathname.split("/").filter(Boolean);
if (!firstSegment) return false;
return !MENU_PREVIEW_RESERVED_SEGMENTS.has(firstSegment);
};
export const toMenuPreviewProxyPath = (slug: string) => {
const normalizedOrigin = MENU_SITE_ORIGIN.replace(/\/+$/, "");
const normalizedSlug = slug.replace(/^\/+/, "");
return `${normalizedOrigin}/${normalizedSlug}`;
};
export const MENU_PREVIEW_CACHE_PARAM = "_preview";
export const buildMenuPreviewUrl = (slug: string, cacheKey: number) => `${toMenuPreviewProxyPath(slug)}?${MENU_PREVIEW_CACHE_PARAM}=${cacheKey}`;
export const stripMenuPreviewCacheParam = (search: string) => {
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
params.delete(MENU_PREVIEW_CACHE_PARAM);
const query = params.toString();
return query ? `?${query}` : "";
};
export const toMenuSitePath = (pathname: string) => {
if (isMenuPreviewProxyPath(pathname)) {
return pathname.replace(/^\/menu-preview/, "") || "/";
}
if (isMenuSlugProxyPath(pathname)) {
return pathname;
}
return null;
};
+31 -31
View File
@@ -1,56 +1,56 @@
import Tabs from '@/components/Tabs' import Tabs from "@/components/Tabs";
import { Money2, NotificationStatus, Setting4 } from 'iconsax-react' import { Brush, Money2, NotificationStatus, Setting4 } from "iconsax-react";
import { useState, type FC } from 'react' import { useState, type FC } from "react";
import { SettingTabEnum } from './enum/Enum' import NotificationsList from "../notifications/List";
import GeneralSettings from './GeneralSettings' import DesignSettings from "./components/DesignSettings";
import ScoreSetting from './components/ScoreSetting' import ScoreSetting from "./components/ScoreSetting";
import NotificationsList from '../notifications/List' import { SettingTabEnum } from "./enum/Enum";
import GeneralSettings from "./GeneralSettings";
const Setting: FC = () => { const Setting: FC = () => {
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.GENERAL);
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.GENERAL)
return ( return (
<div className='mt-5'> <div className="mt-5">
<h1 className='text-lg font-light'>تنظیمات</h1> <h1 className="text-lg font-light">تنظیمات</h1>
<div className='mt-8'> <div className="mt-8">
<Tabs <Tabs
items={[ items={[
{ {
icon: <Setting4 color={activeTab === SettingTabEnum.GENERAL ? 'black' : '#8C90A3'} size={24} />, icon: <Setting4 color={activeTab === SettingTabEnum.GENERAL ? "black" : "#8C90A3"} size={24} />,
label: 'عمومی', label: "عمومی",
value: SettingTabEnum.GENERAL, value: SettingTabEnum.GENERAL,
}, },
{ {
icon: <Money2 color={activeTab === SettingTabEnum.FINANCIAL ? 'black' : '#8C90A3'} size={24} />, icon: <Money2 color={activeTab === SettingTabEnum.FINANCIAL ? "black" : "#8C90A3"} size={24} />,
label: 'تنظیمات امتیاز', label: "تنظیمات امتیاز",
value: SettingTabEnum.FINANCIAL, value: SettingTabEnum.FINANCIAL,
}, },
{ {
icon: <NotificationStatus color={activeTab === SettingTabEnum.NOTIFICATION ? 'black' : '#8C90A3'} size={24} />, icon: <NotificationStatus color={activeTab === SettingTabEnum.NOTIFICATION ? "black" : "#8C90A3"} size={24} />,
label: 'تنظیمات اعلان', label: "تنظیمات اعلان",
value: SettingTabEnum.NOTIFICATION, value: SettingTabEnum.NOTIFICATION,
}, },
{
icon: <Brush color={activeTab === SettingTabEnum.BACKGROUND ? "black" : "#8C90A3"} size={24} />,
label: "تنظیمات طراحی",
value: SettingTabEnum.BACKGROUND,
},
]} ]}
onChange={(value: string) => setActiveTab(value as SettingTabEnum)} onChange={(value: string) => setActiveTab(value as SettingTabEnum)}
active={activeTab} active={activeTab}
/> />
</div> </div>
<div className='mt-8'> <div className="mt-8">
{ {activeTab === SettingTabEnum.GENERAL && <GeneralSettings />}
activeTab === SettingTabEnum.GENERAL && <GeneralSettings /> {activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />}
} {activeTab === SettingTabEnum.NOTIFICATION && <NotificationsList />}
{ {activeTab === SettingTabEnum.BACKGROUND && <DesignSettings />}
activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />
}
{
activeTab === SettingTabEnum.NOTIFICATION && <NotificationsList />
}
</div> </div>
</div> </div>
) );
} };
export default Setting export default Setting;
@@ -0,0 +1,392 @@
import iphoneCamera from "@/assets/images/iphone_camera.png";
import iphoneFrame from "@/assets/images/iphonr.png";
import Button from "@/components/Button";
import ColorPicker from "@/components/ColorPicker";
import Radio from "@/components/Radio";
import { extractErrorMessage } from "@/config/func";
import { buildMenuPreviewUrl } from "@/config/menuPreview";
import type { ErrorType } from "@/helpers/types";
import { clx } from "@/helpers/utils";
import DesignSlider from "@/pages/settings/components/DesignSlider";
import { getBackgroundPatternUrl, PATTERN_BACKGROUND_OPACITY, useBackgroundPatterns } from "@/pages/settings/hooks/usePatterns";
import { useGetBackgrounds, useGetRestaurant, useSetBackground, useUpdateRestaurant } from "@/pages/settings/hooks/useSettingData";
import type { BackgroundItemType, SetBackgroundType } from "@/pages/settings/types/Types";
import { useSingleUpload } from "@/pages/uploader/hooks/useUploaderData";
import { DocumentUpload, Gallery, TickCircle, Trash } from "iconsax-react";
import { useCallback, useEffect, useMemo, useState, type FC } from "react";
import { useDropzone } from "react-dropzone";
import { toast } from "react-toastify";
const COLOR_APPLY_DELAY_MS = 500;
type BackgroundType = "pattern" | "custom";
const UPLOAD_BOX_CLASS = "w-[200px] h-[362px] rounded-2xl border border-dashed border-description";
const PHONE_FRAME_WIDTH = 375;
const PHONE_PREVIEW_SCALE = 0.8;
/** Measured from `iphonr.png` (839×1759) — white screen rect inside the bezel. */
const PHONE_FRAME_IMAGE = { width: 839, height: 1759 } as const;
const PHONE_SCREEN_RECT = { left: 33, top: 36, right: 806, bottom: 1756, radius: 89 } as const;
const toFramePercent = (value: number, axis: "width" | "height") => `${(value / PHONE_FRAME_IMAGE[axis]) * 100}%`;
const PHONE_SCREEN_STYLE = {
top: toFramePercent(PHONE_SCREEN_RECT.top, "height"),
left: toFramePercent(PHONE_SCREEN_RECT.left, "width"),
right: toFramePercent(PHONE_FRAME_IMAGE.width - 1 - PHONE_SCREEN_RECT.right, "width"),
bottom: toFramePercent(PHONE_FRAME_IMAGE.height - 1 - PHONE_SCREEN_RECT.bottom, "height"),
borderRadius: (PHONE_SCREEN_RECT.radius / PHONE_FRAME_IMAGE.width) * PHONE_FRAME_WIDTH,
} as const;
const PHONE_CAMERA_STYLE = {
top: toFramePercent(18, "height"),
width: toFramePercent(178, "width"),
} as const;
const getPatternBgUrl = (item: BackgroundItemType) => item.url ?? item.bgUrl ?? "";
const normalizeBgUrl = (url: string) => {
if (!url) return "";
if (url.startsWith("http")) return url;
return `${import.meta.env.VITE_BASE_URL}${url.startsWith("/") ? url : `/${url}`}`;
};
const resolveBgType = (type: BackgroundType, selectedPatternId: string | null): SetBackgroundType["bgType"] => {
if (type === "custom") return "custom";
if (selectedPatternId === "0") return "color";
return "pattern";
};
const DesignSettings: FC = () => {
const [menuColor, setMenuColor] = useState("#000000");
const [appliedColor, setAppliedColor] = useState("#000000");
const [selectedPatternId, setSelectedPatternId] = useState<string | null>(null);
const [backgroundType, setBackgroundType] = useState<BackgroundType>("pattern");
const [customImage, setCustomImage] = useState<File | null>(null);
const [customImagePreview, setCustomImagePreview] = useState<string | null>(null);
const [existingBgUrl, setExistingBgUrl] = useState<string | null>(null);
const [imageOpacity, setImageOpacity] = useState(100);
const [imageBlur, setImageBlur] = useState(0);
const [overlayDarkness, setOverlayDarkness] = useState(0);
const [isInitialized, setIsInitialized] = useState(false);
const [previewReloadKey, setPreviewReloadKey] = useState(0);
const { data: backgrounds } = useGetBackgrounds();
const { mutate: setBackground, isPending: isSavingBackground } = useSetBackground();
const { mutate: updateRestaurant, isPending: isUpdatingRestaurant } = useUpdateRestaurant();
const { mutate: singleUpload, isPending: isUploadingImage } = useSingleUpload();
const { patterns, isLoading: isPatternsLoading, isError: isPatternsError } = useBackgroundPatterns(appliedColor);
const { data: restaurant, refetch } = useGetRestaurant();
const backgroundItems = useMemo<BackgroundItemType[]>(() => backgrounds?.data ?? [], [backgrounds]);
const menuPreviewUrl = useMemo(() => {
const slug = restaurant?.data?.slug;
if (!slug) return "";
return buildMenuPreviewUrl(slug, previewReloadKey);
}, [restaurant?.data?.slug, previewReloadKey]);
const isSaving = isSavingBackground || isUploadingImage || isUpdatingRestaurant;
useEffect(() => {
if (!restaurant?.data || isInitialized) return;
const restaurantData = restaurant.data;
const savedBgUrl = restaurantData.bgUrl;
if (savedBgUrl && backgroundItems.length === 0) return;
const color = restaurantData.menuColor || "#000000";
setMenuColor(color);
setAppliedColor(color);
const savedBgType = restaurantData.bgType;
if (savedBgType === "color" || (!savedBgUrl && savedBgType !== "custom" && savedBgType !== "image")) {
setBackgroundType("pattern");
setSelectedPatternId("0");
} else if (savedBgUrl) {
const matchingPattern = backgroundItems.find((item) => getBackgroundPatternUrl(item) === normalizeBgUrl(savedBgUrl) || getPatternBgUrl(item) === savedBgUrl);
if (matchingPattern && savedBgType !== "custom" && savedBgType !== "image") {
setBackgroundType("pattern");
setSelectedPatternId(matchingPattern.id);
} else {
setBackgroundType("custom");
setExistingBgUrl(savedBgUrl);
setCustomImagePreview(normalizeBgUrl(savedBgUrl));
setImageOpacity(Number(restaurantData.bgOpacity) || 100);
setImageBlur(Number(restaurantData.bgBlur) || 0);
setOverlayDarkness(Number(restaurantData.bgOverlay) || 0);
}
} else {
setSelectedPatternId("0");
}
setIsInitialized(true);
}, [restaurant, backgroundItems, isInitialized]);
useEffect(() => {
const timer = setTimeout(() => setAppliedColor(menuColor), COLOR_APPLY_DELAY_MS);
return () => clearTimeout(timer);
}, [menuColor]);
useEffect(() => {
if (!customImage) return;
const previewUrl = URL.createObjectURL(customImage);
setCustomImagePreview(previewUrl);
return () => URL.revokeObjectURL(previewUrl);
}, [customImage]);
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles[0]) {
setCustomImage(acceptedFiles[0]);
}
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: { "image/*": [] },
multiple: false,
});
const handleRemoveImage = () => {
setCustomImage(null);
setExistingBgUrl(null);
setCustomImagePreview(null);
setImageOpacity(100);
setImageBlur(0);
setOverlayDarkness(0);
};
const handleSaveSuccess = useCallback(() => {
toast.success("تنظیمات طراحی با موفقیت ذخیره شد");
refetch();
setPreviewReloadKey(Date.now());
}, [refetch]);
const saveCustomBackground = (bgUrl: string) => {
const backgroundPayload: SetBackgroundType = {
bgUrl,
bgType: "custom",
bgBlur: String(imageBlur ?? 0),
bgOverlay: String(overlayDarkness ?? 0),
};
if (imageOpacity) {
backgroundPayload.bgOpacity = String(imageOpacity);
}
setBackground(backgroundPayload, {
onSuccess: handleSaveSuccess,
onError: (error: unknown) => {
toast.error(extractErrorMessage(error as ErrorType, "خطا در ذخیره تنظیمات طراحی"));
},
});
};
const handleApplyChanges = () => {
updateRestaurant(
{ menuColor },
{
onSuccess: () => {
if (backgroundType === "pattern") {
let bgUrl = "";
if (selectedPatternId !== "0") {
const selectedItem = backgroundItems.find((item) => item.id === selectedPatternId);
bgUrl = selectedItem ? getPatternBgUrl(selectedItem) : "";
if (!bgUrl) {
toast.error("لطفاً یک پترن انتخاب کنید");
return;
}
}
setBackground(
{
bgUrl,
bgType: resolveBgType(backgroundType, selectedPatternId),
},
{
onSuccess: handleSaveSuccess,
onError: (error: unknown) => {
toast.error(extractErrorMessage(error as ErrorType, "خطا در ذخیره تنظیمات طراحی"));
},
},
);
return;
}
if (customImage) {
singleUpload(customImage, {
onSuccess: (response) => {
const bgUrl = response?.data?.url ?? "";
if (!bgUrl) {
toast.error("خطا در آپلود تصویر");
return;
}
saveCustomBackground(bgUrl);
},
onError: (error: ErrorType) => {
toast.error(extractErrorMessage(error, "خطا در آپلود تصویر"));
},
});
return;
}
if (existingBgUrl) {
saveCustomBackground(existingBgUrl);
return;
}
toast.error("لطفاً تصویر پس‌زمینه را آپلود کنید");
},
onError: (error: unknown) => {
toast.error(extractErrorMessage(error as ErrorType, "خطا در ذخیره تنظیمات طراحی"));
},
},
);
};
const isApplyDisabled = isSaving || (backgroundType === "pattern" ? !selectedPatternId : !customImage && !existingBgUrl);
return (
<div className="bg-white rounded-4xl p-8">
<div className="text-lg font-light">تنظیمات طراحی</div>
<div className="flex mt-5 gap-10">
<div className="max-w-[506px] w-full shrink-0">
<ColorPicker label="رنگ منو" value={menuColor} onChange={setMenuColor} />
<div className="mt-7">
<div className="flex gap-2 items-center">
<Radio isActive={backgroundType === "pattern"} value="pattern" onChange={(value) => setBackgroundType(value as BackgroundType)} />
<div className="text-sm">انتخاب پس زمینه</div>
</div>
{backgroundType === "pattern" && (
<div className="mt-2 flex gap-3 flex-wrap">
{isPatternsLoading && Array.from({ length: 6 }).map((_, index) => <div key={index} className="w-16 h-[139px] rounded-lg bg-secondary animate-pulse" />)}
{!isPatternsLoading && isPatternsError && <div className="text-sm text-description">خطا در بارگذاری پترنها</div>}
{!isPatternsLoading && !isPatternsError && patterns.length === 0 && <div className="text-sm text-description">پترنی یافت نشد</div>}
<button
onClick={() => setSelectedPatternId("0")}
type="button"
className={clx("relative w-16 h-[139px] rounded-lg bg-white border overflow-hidden", selectedPatternId === "0" ? "border-black" : "border-border")}
>
<div className="absolute inset-0" style={{ backgroundColor: appliedColor, opacity: PATTERN_BACKGROUND_OPACITY }} />
</button>
{patterns.map((pattern) => (
<button
key={pattern.id}
type="button"
onClick={() => setSelectedPatternId(pattern.id)}
className={clx("relative w-16 h-[139px] rounded-lg bg-white border overflow-hidden", selectedPatternId === pattern.id ? "border-black" : "border-border")}
>
<div className="absolute inset-0" style={{ backgroundColor: appliedColor, opacity: PATTERN_BACKGROUND_OPACITY }} />
<img src={pattern.image} alt="" className="relative scale-200 w-full h-full object-cover" />
</button>
))}
</div>
)}
</div>
<div className="mt-7">
<div className="flex gap-2 items-center">
<Radio isActive={backgroundType === "custom"} value="custom" onChange={(value) => setBackgroundType(value as BackgroundType)} />
<div className="text-sm">تصویر پس زمینه دلخواه</div>
</div>
{backgroundType === "custom" && (
<div className="mt-2">
{!customImagePreview ? (
<div {...getRootProps()} className={clx(UPLOAD_BOX_CLASS, "flex flex-col items-center justify-center gap-3 cursor-pointer bg-white")}>
<input {...getInputProps()} />
<Gallery className="size-8" color="#8C90A3" />
<div className="text-description text-xs text-center leading-5 px-4">تصویر مورد نظر را آپلود کنید</div>
<div className="flex items-center gap-1">
<DocumentUpload className="size-4" color="black" />
<div className="text-xs">آپلود</div>
</div>
</div>
) : (
<div className="flex gap-8 items-start">
<div className={clx(UPLOAD_BOX_CLASS, "relative shrink-0 overflow-hidden")}>
<img
src={customImagePreview}
alt="پس‌زمینه دلخواه"
className="w-full h-full object-cover"
style={{
opacity: imageOpacity / 100,
filter: `blur(${imageBlur * 0.2}px)`,
}}
/>
{overlayDarkness > 0 && <div className="absolute inset-0 bg-black pointer-events-none" style={{ opacity: overlayDarkness / 100 }} />}
<button
type="button"
onClick={handleRemoveImage}
className="absolute top-1.5 right-1.5 size-5 rounded-full bg-white shadow-sm flex items-center justify-center cursor-pointer z-10"
>
<Trash size={12} color="#EF4444" variant="Bold" />
</button>
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-normal mb-4">تنظیمات</div>
<DesignSlider label="شفافیت تصویر" value={imageOpacity} onChange={setImageOpacity} />
<DesignSlider label="محوشدگی تصویر" value={imageBlur} onChange={setImageBlur} />
<DesignSlider label="تیرگی روکش پس زمینه" value={overlayDarkness} onChange={setOverlayDarkness} />
</div>
</div>
)}
</div>
)}
</div>
<div className="mt-8 flex justify-end">
<Button type="button" className="w-fit px-6" isloading={isSaving} disabled={isApplyDisabled} onClick={handleApplyChanges}>
<div className="flex gap-2 items-center">
<TickCircle size={20} color="#fff" />
<span>اعمال تغییرات</span>
</div>
</Button>
</div>
</div>
<div className="flex-1 flex justify-center items-start min-w-0 sticky top-4">
<div className="relative shrink-0 select-none" style={{ width: PHONE_FRAME_WIDTH * PHONE_PREVIEW_SCALE }}>
<div className="relative overflow-hidden" style={{ width: PHONE_FRAME_WIDTH, transform: `scale(${PHONE_PREVIEW_SCALE})`, transformOrigin: "top center" }}>
<img src={iphoneFrame} alt="" className="relative z-0 w-full h-auto block pointer-events-none" draggable={false} />
<div
className="absolute z-10 overflow-hidden"
style={{
top: PHONE_SCREEN_STYLE.top,
left: PHONE_SCREEN_STYLE.left,
right: PHONE_SCREEN_STYLE.right,
bottom: PHONE_SCREEN_STYLE.bottom,
borderRadius: PHONE_SCREEN_STYLE.borderRadius,
}}
>
{menuPreviewUrl ? (
<iframe key={previewReloadKey} src={menuPreviewUrl} title="پیش‌نمایش منو" className="block h-full w-full border-0 bg-white" />
) : (
<div className="flex h-full w-full items-center justify-center bg-white text-description text-xs">در حال بارگذاری...</div>
)}
</div>
<img
src={iphoneCamera}
alt=""
className="pointer-events-none absolute left-1/2 z-20 -translate-x-1/2"
style={{ top: PHONE_CAMERA_STYLE.top, width: PHONE_CAMERA_STYLE.width }}
draggable={false}
/>
</div>
</div>
</div>
</div>
</div>
);
};
export default DesignSettings;
@@ -0,0 +1,41 @@
import { type FC } from "react";
type Props = {
label: string;
value: number;
onChange: (value: number) => void;
};
const DesignSlider: FC<Props> = ({ label, value, onChange }) => {
return (
<div className="mb-5 last:mb-0">
<div className="text-sm mb-3">{label}</div>
<div className="relative pt-1 pb-5">
<div className="relative h-1">
<div className="absolute inset-0 top-1/2 -translate-y-1/2 h-px bg-border" />
<div
className="absolute top-1/2 -translate-y-1/2 right-0 bg-black"
style={{
width: `${value}%`,
height: value > 0 ? 2 : 1,
}}
/>
<input
type="range"
min={0}
max={100}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
dir="rtl"
className="design-slider absolute inset-0 w-full h-full appearance-none bg-transparent cursor-pointer"
/>
</div>
<span className="absolute text-xs -bottom-2 -translate-x-1/2" style={{ right: `calc(${value}% - 10px)` }}>
{value.toLocaleString("fa-IR")}%
</span>
</div>
</div>
);
};
export default DesignSlider;
+1
View File
@@ -4,4 +4,5 @@ export const enum SettingTabEnum {
FINANCIAL = "financial", FINANCIAL = "financial",
DELIVERY = "delivery", DELIVERY = "delivery",
NOTIFICATION = "notification", NOTIFICATION = "notification",
BACKGROUND = "background",
} }
+129
View File
@@ -0,0 +1,129 @@
import type { BackgroundItemType } from "@/pages/settings/types/Types";
import { useEffect, useMemo, useState } from "react";
import { useGetBackgrounds } from "./useSettingData";
import firstPattern from "@/assets/images/pattern/1.svg?raw";
import secondPattern from "@/assets/images/pattern/2.svg?raw";
import thirdPattern from "@/assets/images/pattern/3.svg?raw";
import fourthPattern from "@/assets/images/pattern/4.svg?raw";
import fifthPattern from "@/assets/images/pattern/5.svg?raw";
import sixthPattern from "@/assets/images/pattern/6.svg?raw";
const PATTERNS = [
{ id: 1, name: "طرح 1", svg: firstPattern },
{ id: 2, name: "طرح 2", svg: secondPattern },
{ id: 3, name: "طرح 3", svg: thirdPattern },
{ id: 4, name: "طرح 4", svg: fourthPattern },
{ id: 5, name: "طرح 5", svg: fifthPattern },
{ id: 6, name: "طرح 6", svg: sixthPattern },
];
export const PATTERN_BACKGROUND_OPACITY = 0.05;
export const PATTERN_VECTOR_OPACITY = 0.1;
export const getBackgroundPatternUrl = (item: BackgroundItemType) => {
const url = item.url ?? item.bgUrl ?? "";
if (!url) return "";
if (url.startsWith("http")) return url;
return `${import.meta.env.VITE_BASE_URL}${url.startsWith("/") ? url : `/${url}`}`;
};
export const colorizeSvg = (svg: string, color: string) => {
let result = svg.replace(/fill="(?!black|none)[^"]+"/g, `fill="${color}"`);
result = result.replace(/fill-opacity="[^"]+"/g, `fill-opacity="${PATTERN_VECTOR_OPACITY}"`);
result = result.replace(
/(<(?:path|circle|rect|ellipse|polygon)[^>]*fill="(?!black|none)[^"]+")(?![^>]*fill-opacity)/g,
`$1 fill-opacity="${PATTERN_VECTOR_OPACITY}"`,
);
return result;
};
export const svgToDataUrl = (svg: string) =>
`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
type CachedPatternSource = {
id: string;
svg?: string;
fallbackUrl?: string;
};
const fetchPatternSource = async (item: BackgroundItemType): Promise<CachedPatternSource | null> => {
const url = getBackgroundPatternUrl(item);
if (!url) return null;
try {
const response = await fetch(url);
if (!response.ok) {
return { id: item.id, fallbackUrl: url };
}
const content = await response.text();
if (!content.includes("<svg")) {
return { id: item.id, fallbackUrl: url };
}
return { id: item.id, svg: content };
} catch {
return { id: item.id, fallbackUrl: url };
}
};
export type BackgroundPattern = {
id: string;
image: string;
};
export const useBackgroundPatterns = (color: string) => {
const { data: backgrounds, isLoading, isError } = useGetBackgrounds();
const [sources, setSources] = useState<CachedPatternSource[]>([]);
const [isFetching, setIsFetching] = useState(false);
const backgroundItems = useMemo(() => backgrounds?.data ?? [], [backgrounds]);
useEffect(() => {
if (!backgroundItems.length) {
setSources([]);
setIsFetching(false);
return;
}
let cancelled = false;
setIsFetching(true);
Promise.all(backgroundItems.map(fetchPatternSource)).then((results) => {
if (cancelled) return;
setSources(results.filter((item): item is CachedPatternSource => item !== null));
setIsFetching(false);
});
return () => {
cancelled = true;
};
}, [backgroundItems]);
const patterns = useMemo(
() =>
sources.map((source) => ({
id: source.id,
image: source.svg ? svgToDataUrl(colorizeSvg(source.svg, color)) : (source.fallbackUrl ?? ""),
})),
[sources, color],
);
return {
patterns,
isLoading: isLoading || isFetching,
isError,
};
};
export const usePatterns = (color: string) => {
return useMemo(
() =>
PATTERNS.map((pattern) => ({
id: pattern.id,
name: pattern.name,
image: svgToDataUrl(colorizeSvg(pattern.svg, color)),
})),
[color],
);
};
@@ -13,3 +13,16 @@ export const useUpdateRestaurant = () => {
mutationFn: api.updateRestaurant, mutationFn: api.updateRestaurant,
}); });
}; };
export const useGetBackgrounds = () => {
return useQuery({
queryKey: ["backgrounds"],
queryFn: api.getBackgrounds,
});
};
export const useSetBackground = () => {
return useMutation({
mutationFn: api.setBackground,
});
};
+11 -1
View File
@@ -1,5 +1,5 @@
import axios from "@/config/axios"; import axios from "@/config/axios";
import type { GetRestaurantResponse, UpdateRestaurantType } from "../types/Types"; import type { GetRestaurantResponse, SetBackgroundType, UpdateRestaurantType } from "../types/Types";
export const getRestaurant = async (): Promise<GetRestaurantResponse> => { export const getRestaurant = async (): Promise<GetRestaurantResponse> => {
const { data } = await axios.get<GetRestaurantResponse>("/admin/shops/my-shop"); const { data } = await axios.get<GetRestaurantResponse>("/admin/shops/my-shop");
@@ -10,3 +10,13 @@ export const updateRestaurant = async (params: UpdateRestaurantType) => {
const { data } = await axios.patch("/admin/shops/my-shop", params); const { data } = await axios.patch("/admin/shops/my-shop", params);
return data; return data;
}; };
export const getBackgrounds = async () => {
const { data } = await axios.get("/admin/backgrounds");
return data;
};
export const setBackground = async (params: SetBackgroundType) => {
const { data } = await axios.patch(`/admin/shops/my-shop/background`, params);
return data;
};
+19
View File
@@ -59,6 +59,11 @@ export type Restaurant = {
images: unknown | null; images: unknown | null;
vat: number; vat: number;
domain: string; domain: string;
bgType: string | null;
bgUrl: string | null;
bgOpacity: string | number | null;
bgBlur: string | number | null;
bgOverlay: string | number | null;
score: { score: {
scoreAmount: string; scoreAmount: string;
scoreCredit: string; scoreCredit: string;
@@ -72,3 +77,17 @@ export type Restaurant = {
}; };
export type GetRestaurantResponse = IResponse<Restaurant>; export type GetRestaurantResponse = IResponse<Restaurant>;
export type SetBackgroundType = {
bgType?: string;
bgUrl?: string;
bgOpacity?: string;
bgBlur?: string;
bgOverlay?: string;
};
export type BackgroundItemType = {
id: string;
url?: string;
bgUrl?: string;
};