Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab87001c77 | ||
|
|
ef4db213c7 | ||
|
|
9ba09fb55b | ||
|
|
4b4e31a55f | ||
|
|
9b2ae99df6 | ||
|
|
af9f92478e | ||
|
|
affb1b1ad8 | ||
|
|
a8d46921f9 | ||
|
|
5eee968d1c | ||
|
|
dcee392e02 | ||
|
|
8803dfa49a | ||
|
|
52a3bb9ab7 | ||
|
|
ed5eead80f | ||
|
|
df6cb33731 | ||
|
|
f315ee56a1 | ||
|
|
3f344fa1a8 | ||
|
|
1d5b638f3e | ||
|
|
aa6cb26b46 | ||
|
|
fe1f17f740 | ||
|
|
494fc9f79b | ||
|
|
d631174d40 | ||
|
|
121287e105 | ||
|
|
fbacd2ca22 | ||
|
|
711ac3668c | ||
|
|
5594c3c25b |
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
docs
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
npm-debug.log
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
name: Build & Deploy (develop)
|
||||||
|
|
||||||
|
# Runs on:
|
||||||
|
# - Pull requests targeting develop
|
||||||
|
# - Direct pushes to develop
|
||||||
|
# main is intentionally NOT included yet (no prod target configured)
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Resolve commit SHA
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
# For PRs, gitea.sha is a temporary merge commit — use the real
|
||||||
|
# PR head commit instead so tags map to actual git history.
|
||||||
|
FULL_SHA="${{ gitea.event.pull_request.head.sha || gitea.sha }}"
|
||||||
|
echo "sha=${FULL_SHA:0:7}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build Docker image (tagged by commit SHA)
|
||||||
|
run: |
|
||||||
|
docker build -t robot-market:${{ steps.vars.outputs.sha }} .
|
||||||
|
|
||||||
|
- name: Save previous tag for rollback
|
||||||
|
run: |
|
||||||
|
if [ -f /opt/robot-market-front/.env ]; then
|
||||||
|
cp /opt/robot-market-front/.env /opt/robot-market-front/.env.previous
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Point compose at this build
|
||||||
|
run: |
|
||||||
|
echo "IMAGE_TAG=${{ steps.vars.outputs.sha }}" > /opt/robot-market-front/.env
|
||||||
|
|
||||||
|
- name: Deploy via compose
|
||||||
|
run: |
|
||||||
|
docker compose --env-file /opt/robot-market-front/.env \
|
||||||
|
-f /opt/robot-market-front/docker-compose.yml \
|
||||||
|
up -d --force-recreate
|
||||||
|
|
||||||
|
- name: Health check (retries for ~30s)
|
||||||
|
run: |
|
||||||
|
for i in $(seq 1 15); do
|
||||||
|
if docker exec robot-market node -e "
|
||||||
|
require('http').get('http://localhost:3000', res => {
|
||||||
|
process.exit(res.statusCode < 400 ? 0 : 1);
|
||||||
|
}).on('error', () => process.exit(1));
|
||||||
|
" 2>/dev/null; then
|
||||||
|
echo "Container is healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Not ready yet, retrying..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Health check failed after 30s"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Roll back to previous version on failed health check
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
if [ -f /opt/robot-market-front/.env.previous ]; then
|
||||||
|
cp /opt/robot-market-front/.env.previous /opt/robot-market-front/.env
|
||||||
|
docker compose --env-file /opt/robot-market-front/.env \
|
||||||
|
-f /opt/robot-market-front/docker-compose.yml \
|
||||||
|
up -d --force-recreate
|
||||||
|
echo "Rolled back to previous image."
|
||||||
|
else
|
||||||
|
echo "No previous deploy recorded — nothing to roll back to."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Clean up dangling (untagged) layers
|
||||||
|
run: docker image prune -f
|
||||||
|
|
||||||
|
- name: Prune old SHA-tagged images (keep last 5)
|
||||||
|
run: |
|
||||||
|
KEEP=5
|
||||||
|
CURRENT_TAG=$(grep IMAGE_TAG /opt/robot-market-front/.env 2>/dev/null | cut -d= -f2)
|
||||||
|
PREVIOUS_TAG=$(grep IMAGE_TAG /opt/robot-market-front/.env.previous 2>/dev/null | cut -d= -f2)
|
||||||
|
|
||||||
|
docker images robot-market --format '{{.Tag}} {{.CreatedAt}}' \
|
||||||
|
| sort -t" " -k2 -r \
|
||||||
|
| tail -n +$((KEEP + 1)) \
|
||||||
|
| cut -f1 \
|
||||||
|
| while read -r tag; do
|
||||||
|
if [ "$tag" != "$CURRENT_TAG" ] && [ "$tag" != "$PREVIOUS_TAG" ] && [ "$tag" != "<none>" ]; then
|
||||||
|
echo "Removing old image robot-market:$tag"
|
||||||
|
docker rmi "robot-market:$tag" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Rollback (develop)
|
||||||
|
|
||||||
|
# Trigger manually from the Gitea Actions UI.
|
||||||
|
# Leave "sha" empty to roll back to the previous deploy.
|
||||||
|
# Fill "sha" in (short 7-char form, e.g. a3f9c21) to roll back to a specific past build.
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
sha:
|
||||||
|
description: "Short commit SHA to roll back to (empty = previous deploy)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rollback:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Determine target tag
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ gitea.event.inputs.sha }}" ]; then
|
||||||
|
TAG="${{ gitea.event.inputs.sha }}"
|
||||||
|
else
|
||||||
|
TAG=$(grep IMAGE_TAG /opt/robot-market-front/.env.previous | cut -d= -f2)
|
||||||
|
fi
|
||||||
|
echo "Rolling back to: $TAG"
|
||||||
|
echo "IMAGE_TAG=$TAG" > /opt/robot-market-front/.env
|
||||||
|
|
||||||
|
- name: Verify that image actually exists locally
|
||||||
|
run: |
|
||||||
|
TAG=$(grep IMAGE_TAG /opt/robot-market-front/.env | cut -d= -f2)
|
||||||
|
docker image inspect robot-market:$TAG > /dev/null
|
||||||
|
|
||||||
|
- name: Redeploy target image
|
||||||
|
run: |
|
||||||
|
docker compose --env-file /opt/robot-market-front/.env \
|
||||||
|
-f /opt/robot-market-front/docker-compose.yml \
|
||||||
|
up -d --force-recreate
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Runner Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
workflow_dispatch: # lets you trigger it manually from the UI too
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
hello:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Print basic info
|
||||||
|
run: |
|
||||||
|
echo "✅ Runner is working!"
|
||||||
|
echo "Hostname: $(hostname)"
|
||||||
|
echo "Date: $(date)"
|
||||||
|
echo "User: $(whoami)"
|
||||||
|
pwd
|
||||||
|
ls -la
|
||||||
|
|
||||||
|
- name: Test env variables
|
||||||
|
run: |
|
||||||
|
echo "Branch: ${{ github.ref_name }}"
|
||||||
|
echo "Commit SHA: ${{ github.sha }}"
|
||||||
|
echo "Actor: ${{ github.actor }}"
|
||||||
|
|
||||||
|
- name: Simulate a build step
|
||||||
|
run: |
|
||||||
|
mkdir -p build
|
||||||
|
echo "Build artifact content" > build/output.txt
|
||||||
|
cat build/output.txt
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptLibraryMappings">
|
||||||
|
<includedPredefinedLibrary name="Node.js Core" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||||
|
<state>
|
||||||
|
<map>
|
||||||
|
<entry key="GitHub Workflow">
|
||||||
|
<value>
|
||||||
|
<SchemaInfo>
|
||||||
|
<option name="name" value="GitHub Workflow" />
|
||||||
|
<option name="relativePathToSchema" value="https://www.schemastore.org/github-workflow.json" />
|
||||||
|
<option name="applicationDefined" value="true" />
|
||||||
|
<option name="patterns">
|
||||||
|
<list>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/test.yml" />
|
||||||
|
</Item>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</SchemaInfo>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="update" />
|
<list default="true" id="bb7b50a5-be9c-4cd7-a05b-979f0e91b173" name="Changes" comment="initial new branch">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/data/content.js" beforeDir="false" afterPath="$PROJECT_DIR$/data/content.js" afterDir="false" />
|
||||||
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
|
<map>
|
||||||
|
<entry key="$PROJECT_DIR$" value="develop" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="JsbtTreeLayoutManager">
|
<component name="JsbtTreeLayoutManager">
|
||||||
@@ -15,6 +26,65 @@
|
|||||||
<scroll-view-position x="0" y="0" />
|
<scroll-view-position x="0" y="0" />
|
||||||
</layout>
|
</layout>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="NextEditCompletionFeaturesState">
|
||||||
|
<decayedCancelled>
|
||||||
|
<entry key="MS100" value="1.7659734554897204" />
|
||||||
|
<entry key="MS500" value="2.6200917385797573" />
|
||||||
|
<entry key="S2" value="2.895672466750576" />
|
||||||
|
<entry key="S5" value="2.9711768722246337" />
|
||||||
|
<entry key="S10" value="3.095911797960576" />
|
||||||
|
<entry key="S30" value="3.6130929483281937" />
|
||||||
|
<entry key="S60" value="4.862542006443887" />
|
||||||
|
<entry key="M2" value="8.222118193057797" />
|
||||||
|
<entry key="M5" value="14.352669475563026" />
|
||||||
|
<entry key="M10" value="18.025488407782152" />
|
||||||
|
<entry key="M15" value="19.665360290707866" />
|
||||||
|
<entry key="M30" value="22.62190726116564" />
|
||||||
|
<entry key="H1" value="26.416500535541072" />
|
||||||
|
<entry key="H2" value="30.051114228090462" />
|
||||||
|
<entry key="H4" value="32.65287419479587" />
|
||||||
|
<entry key="D1" value="35.38129255958758" />
|
||||||
|
<entry key="W1" value="35.9099877286286" />
|
||||||
|
</decayedCancelled>
|
||||||
|
<decayedSelected>
|
||||||
|
<entry key="MS100" value="0.0" />
|
||||||
|
<entry key="MS500" value="1.5499348360687339E-164" />
|
||||||
|
<entry key="S2" value="1.1157794535925266E-41" />
|
||||||
|
<entry key="S5" value="4.1594062872493325E-17" />
|
||||||
|
<entry key="S10" value="6.449345925944224E-9" />
|
||||||
|
<entry key="S30" value="0.0018613950614188026" />
|
||||||
|
<entry key="S60" value="0.043143887880194604" />
|
||||||
|
<entry key="M2" value="0.20771106826597993" />
|
||||||
|
<entry key="M5" value="0.533315069904893" />
|
||||||
|
<entry key="M10" value="0.7302842391185045" />
|
||||||
|
<entry key="M15" value="0.8109510091490459" />
|
||||||
|
<entry key="M30" value="0.9005281834285062" />
|
||||||
|
<entry key="H1" value="0.9489616343290734" />
|
||||||
|
<entry key="H2" value="0.9741466184969663" />
|
||||||
|
<entry key="H4" value="0.986988661787442" />
|
||||||
|
<entry key="D1" value="0.9978195926881375" />
|
||||||
|
<entry key="W1" value="0.9996882217756649" />
|
||||||
|
</decayedSelected>
|
||||||
|
<decayedShown>
|
||||||
|
<entry key="MS100" value="1.6363299214720612" />
|
||||||
|
<entry key="MS500" value="2.580440430635064" />
|
||||||
|
<entry key="S2" value="2.884654206650678" />
|
||||||
|
<entry key="S5" value="2.9666305108034594" />
|
||||||
|
<entry key="S10" value="3.0934710562664876" />
|
||||||
|
<entry key="S30" value="3.6138341741583195" />
|
||||||
|
<entry key="S60" value="4.903918162005831" />
|
||||||
|
<entry key="M2" value="8.426649355082025" />
|
||||||
|
<entry key="M5" value="14.882896956006677" />
|
||||||
|
<entry key="M10" value="18.75367770497998" />
|
||||||
|
<entry key="M15" value="20.474761353109788" />
|
||||||
|
<entry key="M30" value="23.521564444287325" />
|
||||||
|
<entry key="H1" value="27.364990222969524" />
|
||||||
|
<entry key="H2" value="31.025011433737856" />
|
||||||
|
<entry key="H4" value="33.639733831212105" />
|
||||||
|
<entry key="D1" value="36.37908993480534" />
|
||||||
|
<entry key="W1" value="36.90967275725512" />
|
||||||
|
</decayedShown>
|
||||||
|
</component>
|
||||||
<component name="ProjectColorInfo">{
|
<component name="ProjectColorInfo">{
|
||||||
"customColor": "",
|
"customColor": "",
|
||||||
"associatedIndex": 2
|
"associatedIndex": 2
|
||||||
@@ -26,25 +96,38 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent">{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
|
"ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "develop",
|
||||||
|
"javascript.nodejs.core.library.configured.version": "24.20.0",
|
||||||
|
"javascript.nodejs.core.library.typings.version": "24.13.3",
|
||||||
"javascript.preferred.runtime.type.id": "node",
|
"javascript.preferred.runtime.type.id": "node",
|
||||||
"js.debugger.nextJs.config.created.client": "true",
|
"js.debugger.nextJs.config.created.client": "true",
|
||||||
"js.debugger.nextJs.config.created.server": "true",
|
"js.debugger.nextJs.config.created.server": "true",
|
||||||
"last_opened_file_path": "D:/Project/GitHubRepo/robot-market-landing",
|
"kronk.content.tree.expanded.paths": "",
|
||||||
|
"last_opened_file_path": "/home/hamed/Documents/Projects/GItLabRepo/device_web",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "editor.preferences.fonts.default",
|
"settings.editor.selected.configurable": "editor.preferences.fonts.default",
|
||||||
"ts.external.directory.path": "C:\\Program Files\\JetBrains\\WebStorm 2026.1.1\\plugins\\javascript-plugin\\jsLanguageServicesImpl\\external",
|
"ts.external.directory.path": "C:\\Program Files\\JetBrains\\WebStorm 2026.1.1\\plugins\\javascript-plugin\\jsLanguageServicesImpl\\external",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}</component>
|
}</component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="D:\Project\GitHubRepo\robot-market-landing\public\images\icons" />
|
||||||
|
<recent name="D:\Project\GitHubRepo\robot-market-landing\public\images" />
|
||||||
|
<recent name="D:\Project\GitHubRepo\robot-market-landing\app" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
<component name="RunManager" selected="npm.Next.js: server-side">
|
<component name="RunManager" selected="npm.Next.js: server-side">
|
||||||
<configuration name="Next.js: debug client-side" type="JavascriptDebugType" uri="http://localhost:3000/">
|
<configuration name="Next.js: debug client-side" type="JavascriptDebugType" uri="http://localhost:3000/">
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
@@ -63,7 +146,7 @@
|
|||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
<attachedChunks>
|
<attachedChunks>
|
||||||
<set>
|
<set>
|
||||||
<option value="bundled-js-predefined-d6986cc7102b-31caf2ab9e3c-JavaScript-WS-261.23567.141" />
|
<option value="bundled-js-predefined-d6986cc7102b-4abd2a77f808-JavaScript-WS-262.8665.259" />
|
||||||
</set>
|
</set>
|
||||||
</attachedChunks>
|
</attachedChunks>
|
||||||
</component>
|
</component>
|
||||||
@@ -84,6 +167,27 @@
|
|||||||
<workItem from="1785645096071" duration="26000" />
|
<workItem from="1785645096071" duration="26000" />
|
||||||
<workItem from="1785646314677" duration="16621000" />
|
<workItem from="1785646314677" duration="16621000" />
|
||||||
<workItem from="1785730130350" duration="12049000" />
|
<workItem from="1785730130350" duration="12049000" />
|
||||||
|
<workItem from="1785757166936" duration="5000" />
|
||||||
|
<workItem from="1785901279535" duration="17323000" />
|
||||||
|
<workItem from="1785988592310" duration="763000" />
|
||||||
|
<workItem from="1785993398943" duration="489000" />
|
||||||
|
<workItem from="1785995385816" duration="1586000" />
|
||||||
|
<workItem from="1785999660169" duration="117000" />
|
||||||
|
<workItem from="1785999908774" duration="697000" />
|
||||||
|
<workItem from="1786434051896" duration="714000" />
|
||||||
|
<workItem from="1787030163530" duration="2042000" />
|
||||||
|
<workItem from="1787052515900" duration="43000" />
|
||||||
|
<workItem from="1787980069071" duration="1369000" />
|
||||||
|
<workItem from="1787993488084" duration="706000" />
|
||||||
|
<workItem from="1788148958104" duration="1576000" />
|
||||||
|
<workItem from="1788244010046" duration="3900000" />
|
||||||
|
<workItem from="1788412255571" duration="40000" />
|
||||||
|
<workItem from="1788412435362" duration="4637000" />
|
||||||
|
<workItem from="1788423298952" duration="3393000" />
|
||||||
|
<workItem from="1788580635152" duration="776000" />
|
||||||
|
<workItem from="1788592387411" duration="8000" />
|
||||||
|
<workItem from="1788592424522" duration="13793000" />
|
||||||
|
<workItem from="1788670304118" duration="1889000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="update">
|
<task id="LOCAL-00001" summary="update">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -197,15 +301,133 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1785747909967</updated>
|
<updated>1785747909967</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="15" />
|
<task id="LOCAL-00015" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785754086526</created>
|
||||||
|
<option name="number" value="00015" />
|
||||||
|
<option name="presentableId" value="LOCAL-00015" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785754086526</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00016" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785754201946</created>
|
||||||
|
<option name="number" value="00016" />
|
||||||
|
<option name="presentableId" value="LOCAL-00016" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785754201946</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00017" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785755999255</created>
|
||||||
|
<option name="number" value="00017" />
|
||||||
|
<option name="presentableId" value="LOCAL-00017" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785755999255</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00018" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785914887554</created>
|
||||||
|
<option name="number" value="00018" />
|
||||||
|
<option name="presentableId" value="LOCAL-00018" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785914887554</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00019" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785915840468</created>
|
||||||
|
<option name="number" value="00019" />
|
||||||
|
<option name="presentableId" value="LOCAL-00019" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785915840468</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00020" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785918100372</created>
|
||||||
|
<option name="number" value="00020" />
|
||||||
|
<option name="presentableId" value="LOCAL-00020" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785918100372</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00021" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785919055708</created>
|
||||||
|
<option name="number" value="00021" />
|
||||||
|
<option name="presentableId" value="LOCAL-00021" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785919055708</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00022" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1785988898095</created>
|
||||||
|
<option name="number" value="00022" />
|
||||||
|
<option name="presentableId" value="LOCAL-00022" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1785988898096</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00023" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1787052543722</created>
|
||||||
|
<option name="number" value="00023" />
|
||||||
|
<option name="presentableId" value="LOCAL-00023" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1787052543722</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00024" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1788245193637</created>
|
||||||
|
<option name="number" value="00024" />
|
||||||
|
<option name="presentableId" value="LOCAL-00024" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1788245193637</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00025" summary="update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1788418070013</created>
|
||||||
|
<option name="number" value="00025" />
|
||||||
|
<option name="presentableId" value="LOCAL-00025" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1788418070013</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00026" summary="feat:dockerized & test actions add">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1788601868240</created>
|
||||||
|
<option name="number" value="00026" />
|
||||||
|
<option name="presentableId" value="LOCAL-00026" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1788601868240</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00027" summary="initial new branch">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1788682674027</created>
|
||||||
|
<option name="number" value="00027" />
|
||||||
|
<option name="presentableId" value="LOCAL-00027" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1788682674027</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00028" summary="initial new branch">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1788682678051</created>
|
||||||
|
<option name="number" value="00028" />
|
||||||
|
<option name="presentableId" value="LOCAL-00028" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1788682678051</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="29" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="UnknownFeatures">
|
||||||
|
<option featureType="com.intellij.fileTypeFactory" implementationName="next.config.js" />
|
||||||
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value="ui : change the position of header in Hero Section" />
|
<MESSAGE value="ui : change the position of header in Hero Section" />
|
||||||
<MESSAGE value="update" />
|
<MESSAGE value="update" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="update" />
|
<MESSAGE value="feat:dockerized & test actions add" />
|
||||||
|
<MESSAGE value="feat:dockerized & test actions added" />
|
||||||
|
<MESSAGE value="initial new branch" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="initial new branch" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
After Width: | Height: | Size: 90 KiB |
@@ -8,12 +8,19 @@ const vazirmatn = Vazirmatn({
|
|||||||
display: 'swap',
|
display: 'swap',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
|
title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
|
||||||
description:
|
description:
|
||||||
'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
|
'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const viewport = {
|
||||||
|
themeColor: '#00a693',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function RootLayout({children}) {
|
export default function RootLayout({children}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import HeroSection from '@/components/HeroSection';
|
import HeroSection from '@/components/HeroSection';
|
||||||
import HorizontalShowcase from '@/components/HorizontalShowcase';
|
|
||||||
import TrustedBySection from '@/components/TrustedBySection';
|
import TrustedBySection from '@/components/TrustedBySection';
|
||||||
import FeaturesSection from '@/components/FeaturesSection';
|
import FeaturesSection from '@/components/FeaturesSection';
|
||||||
import AboutSection from '@/components/AboutSection';
|
import AboutSection from '@/components/AboutSection';
|
||||||
@@ -12,6 +11,7 @@ import TicketForm from '@/components/TicketForm';
|
|||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
import CartDrawer from '@/components/CartDrawer';
|
import CartDrawer from '@/components/CartDrawer';
|
||||||
import ChatWidget from '@/components/ChatWidget';
|
import ChatWidget from '@/components/ChatWidget';
|
||||||
|
import ShowcaseSection from "@/components/ShowcaseSection";
|
||||||
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
@@ -21,27 +21,28 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<CartDrawer/>
|
<CartDrawer/>
|
||||||
|
|
||||||
<ChatWidget/>
|
{/*faz 1*/}
|
||||||
|
{/*<ChatWidget/>*/}
|
||||||
|
|
||||||
<HeroSection/>
|
<HeroSection/>
|
||||||
|
|
||||||
<HorizontalShowcase/>
|
<ShowcaseSection/>
|
||||||
|
|
||||||
<TrustedBySection/>
|
{/*<TrustedBySection/>*/}
|
||||||
|
|
||||||
<FeaturesSection/>
|
<FeaturesSection/>
|
||||||
|
|
||||||
<AboutSection/>
|
<AboutSection/>
|
||||||
|
|
||||||
<CatalogSection/>
|
{/*<CatalogSection/>*/}
|
||||||
|
|
||||||
<LocationsSection/>
|
<LocationsSection/>
|
||||||
|
|
||||||
<IntroVideoSection/>
|
{/*<IntroVideoSection/>*/}
|
||||||
|
|
||||||
<MapSection/>
|
{/*<MapSection/>*/}
|
||||||
|
|
||||||
<TicketForm/>
|
{/*<TicketForm/>*/}
|
||||||
|
|
||||||
<Footer/>
|
<Footer/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -2,38 +2,39 @@
|
|||||||
|
|
||||||
// سکشن معرفی شرکت با متن بلند؛ بهخاطر طولانی بودن متن، یک دکمه «ادامه مطلب» برای باز/بسته کردن دارد.
|
// سکشن معرفی شرکت با متن بلند؛ بهخاطر طولانی بودن متن، یک دکمه «ادامه مطلب» برای باز/بسته کردن دارد.
|
||||||
|
|
||||||
import { useState } from 'react';
|
import {useState} from 'react';
|
||||||
import { useLanguage } from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
|
|
||||||
export default function AboutSection() {
|
export default function AboutSection() {
|
||||||
const { t } = useLanguage();
|
const {t} = useLanguage();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(true);
|
||||||
const visibleParagraphs = expanded ? t.about.paragraphs : t.about.paragraphs.slice(0, 2);
|
const visibleParagraphs = expanded ? t.about.paragraphs : t.about.paragraphs.slice(0, 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="about" className="relative px-6 md:px-16 py-20 md:py-28 bg-elevated border-t border-subtle">
|
<section id="about" className="relative px-6 md:px-16 py-20 md:py-28 bg-elevated border-t border-subtle">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.about.eyebrow}</span>
|
<span className="text-brand-400 text-sm font-semibold tracking-wide">{t.about.eyebrow}</span>
|
||||||
<h2 className="text-2xl md:text-4xl font-extrabold brand-gradient-text mt-3 mb-8">
|
<h2 className="text-2xl md:text-4xl font-extrabold brand-gradient-text mt-3 mb-8">
|
||||||
{t.about.legalName}
|
{t.about.legalName}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
{visibleParagraphs.map((p, i) => (
|
{visibleParagraphs.map((p, i) => (
|
||||||
<p key={i} className="text-secondary leading-8 text-base md:text-lg text-justify">
|
<p key={i} className="text-secondary leading-8 text-base md:text-lg text-justify">
|
||||||
{p}
|
{p}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
{/* faz 1 */}
|
||||||
onClick={() => setExpanded((v) => !v)}
|
{/* <button
|
||||||
className="mt-6 inline-flex items-center gap-2 text-brand-400 hover:text-brand-300 text-sm font-semibold transition-colors"
|
onClick={() => setExpanded((v) => !v)}
|
||||||
>
|
className="mt-6 inline-flex items-center gap-2 text-brand-400 hover:text-brand-300 text-sm font-semibold transition-colors"
|
||||||
{expanded ? t.about.less : t.about.more}
|
>
|
||||||
<span className={`transition-transform ${expanded ? 'rotate-180' : ''}`}>˅</span>
|
{expanded ? t.about.less : t.about.more}
|
||||||
</button>
|
<span className={`transition-transform ${expanded ? 'rotate-180' : ''}`}>˅</span>
|
||||||
</div>
|
</button>*/}
|
||||||
</section>
|
</div>
|
||||||
);
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function CartDrawer() {
|
|||||||
// api() <===> todo : use this func to fetch ...
|
// api() <===> todo : use this func to fetch ...
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
message.info(t.cart.checkoutToast);
|
await message.info(t.cart.checkoutToast);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default function CatalogSection() {
|
|||||||
image: item.image,
|
image: item.image,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
message.success(t.catalog.addToast(item.name));
|
message.success(t.catalog.addToast(item.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,56 +2,56 @@
|
|||||||
|
|
||||||
// رندر آیتمهای شناور (چیپس/نوشابه) دور دستگاه با حرکت تصادفی و ملایم GSAP.
|
// رندر آیتمهای شناور (چیپس/نوشابه) دور دستگاه با حرکت تصادفی و ملایم GSAP.
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import {useEffect, useRef} from 'react';
|
||||||
import { gsap } from 'gsap';
|
import {gsap} from 'gsap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the centralized `floatingSnacks` config as absolutely-positioned
|
* Renders the centralized `floatingSnacks` config as absolutely-positioned
|
||||||
* images and gives each one a gentle, randomized floating drift via GSAP.
|
* images and gives each one a gentle, randomized floating drift via GSAP.
|
||||||
* Positions/sizes/images all come from data/content.js — edit there only.
|
* Positions/sizes/images all come from data/content.js — edit there only.
|
||||||
*/
|
*/
|
||||||
export default function FloatingItems({ items }) {
|
export default function FloatingItems({items}) {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nodes = containerRef.current?.querySelectorAll('[data-float-item]');
|
const nodes = containerRef.current?.querySelectorAll('[data-float-item]');
|
||||||
if (!nodes) return;
|
if (!nodes) return;
|
||||||
|
|
||||||
const tweens = Array.from(nodes).map((node, i) => {
|
const tweens = Array.from(nodes).map((node, i) => {
|
||||||
const drift = 14 + Math.random() * 14;
|
const drift = 14 + Math.random() * 14;
|
||||||
const rotate = 4 + Math.random() * 6;
|
const rotate = 4 + Math.random() * 6;
|
||||||
return gsap.to(node, {
|
return gsap.to(node, {
|
||||||
y: `+=${drift}`,
|
y: `+=${drift}`,
|
||||||
rotate: i % 2 === 0 ? rotate : -rotate,
|
rotate: i % 2 === 0 ? rotate : -rotate,
|
||||||
duration: 3.5 + Math.random() * 2.5,
|
duration: 3.5 + Math.random() * 2.5,
|
||||||
ease: 'sine.inOut',
|
ease: 'sine.inOut',
|
||||||
yoyo: true,
|
yoyo: true,
|
||||||
repeat: -1,
|
repeat: -1,
|
||||||
delay: Math.random() * 1.5,
|
delay: Math.random() * 1.5,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => tweens.forEach((t) => t.kill());
|
return () => tweens.forEach((t) => t.kill());
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="absolute inset-0 pointer-events-none">
|
<div ref={containerRef} className="absolute inset-0 pointer-events-none hidden md:block">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<img
|
<img
|
||||||
key={item.id}
|
key={item.id}
|
||||||
data-float-item
|
data-float-item
|
||||||
src={item.image}
|
src={item.image}
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="absolute drop-shadow-[0_8px_20px_rgba(0,0,0,0.35)]"
|
className="absolute drop-shadow-[0_8px_20px_rgba(0,0,0,0.35)]"
|
||||||
style={{
|
style={{
|
||||||
top: item.top,
|
top: item.top,
|
||||||
left: item.left,
|
left: item.left,
|
||||||
width: item.size,
|
width: item.size,
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ export default function Footer() {
|
|||||||
<li><a href="#hero" className="hover:text-brand-400">{t.nav.home}</a></li>
|
<li><a href="#hero" className="hover:text-brand-400">{t.nav.home}</a></li>
|
||||||
<li><a href="#showcase" className="hover:text-brand-400">{t.nav.showcase}</a></li>
|
<li><a href="#showcase" className="hover:text-brand-400">{t.nav.showcase}</a></li>
|
||||||
<li><a href="#about" className="hover:text-brand-400">{t.nav.about}</a></li>
|
<li><a href="#about" className="hover:text-brand-400">{t.nav.about}</a></li>
|
||||||
<li><a href="#catalog" className="hover:text-brand-400">{t.nav.catalog}</a></li>
|
{/*<li><a href="#catalog" className="hover:text-brand-400">{t.nav.catalog}</a></li>*/}
|
||||||
<li><a href="#locations" className="hover:text-brand-400">{t.nav.locations}</a></li>
|
<li><a href="#locations" className="hover:text-brand-400">{t.nav.locations}</a></li>
|
||||||
<li><a href="#video" className="hover:text-brand-400">{t.nav.video}</a></li>
|
{/*<li><a href="#video" className="hover:text-brand-400">{t.nav.video}</a></li>*/}
|
||||||
<li><a href="#map" className="hover:text-brand-400">{t.nav.map}</a></li>
|
{/*<li><a href="#map" className="hover:text-brand-400">{t.nav.map}</a></li>*/}
|
||||||
<li><a href="#support" className="hover:text-brand-400">{t.nav.support}</a></li>
|
{/*<li><a href="#support" className="hover:text-brand-400">{t.nav.support}</a></li>*/}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export default function Header() {
|
|||||||
{href: '#hero', label: t.nav.home},
|
{href: '#hero', label: t.nav.home},
|
||||||
{href: '#showcase', label: t.nav.showcase},
|
{href: '#showcase', label: t.nav.showcase},
|
||||||
{href: '#about', label: t.nav.about},
|
{href: '#about', label: t.nav.about},
|
||||||
{href: '#catalog', label: t.nav.catalog},
|
// {href: '#catalog', label: t.nav.catalog},
|
||||||
{href: '#locations', label: t.nav.locations},
|
{href: '#locations', label: t.nav.locations},
|
||||||
{href: '#video', label: t.nav.video},
|
// {href: '#video', label: t.nav.video},
|
||||||
{href: '#map', label: t.nav.map},
|
// {href: '#map', label: t.nav.map},
|
||||||
{href: '#support', label: t.nav.support},
|
// {href: '#support', label: t.nav.support},
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,7 +44,7 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<div className="mx-auto max-w-7xl px-5 md:px-8 h-20 flex items-center justify-between gap-4">
|
<div className="mx-auto max-w-7xl px-5 md:px-8 h-20 flex items-center justify-between gap-4">
|
||||||
<a href="#hero" className="flex items-center gap-2 shrink-0">
|
<a href="#hero" className="flex items-center gap-2 shrink-0">
|
||||||
<Image src="/images/logo.png" alt="روبات مارکت" width={150} height={42} priority/>
|
<Image src="/images/icons/logo.png" alt="روبات مارکت" width={150} height={42} priority/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav className="hidden xl:flex items-center gap-5 2xl:gap-7">
|
<nav className="hidden xl:flex items-center gap-5 2xl:gap-7">
|
||||||
@@ -75,7 +75,8 @@ export default function Header() {
|
|||||||
{t.nav.login}
|
{t.nav.login}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Badge count={cartCount} size="small" offset={[-2, 2]}>
|
{/* faz 1 */}
|
||||||
|
{/* <Badge count={cartCount} size="small" offset={[-2, 2]}>
|
||||||
<Button
|
<Button
|
||||||
shape="circle"
|
shape="circle"
|
||||||
size="large"
|
size="large"
|
||||||
@@ -83,7 +84,7 @@ export default function Header() {
|
|||||||
onClick={() => dispatch(openDrawer())}
|
onClick={() => dispatch(openDrawer())}
|
||||||
aria-label="cart"
|
aria-label="cart"
|
||||||
/>
|
/>
|
||||||
</Badge>
|
</Badge>*/}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="xl:hidden text-brand-400 text-2xl"
|
className="xl:hidden text-brand-400 text-2xl"
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// سکشن ویترین محصولات: با اسکرول عمودی به اسکرول افقی پینشده تبدیل میشود (GSAP ScrollTrigger)
|
|
||||||
// و یک ویدیوی پسزمینه دارد که با پیشرفت اسکرول جلو/عقب میرود. طبق درخواست کاربر، این سکشن
|
|
||||||
// و هیرو از چرخهی رنگبندی متناوب بقیه سکشنها مستثنا هستند.
|
|
||||||
|
|
||||||
import {useEffect, useRef, useState} from 'react';
|
import {useEffect, useRef, useState} from 'react';
|
||||||
import {gsap} from 'gsap';
|
import {gsap} from 'gsap';
|
||||||
import {ScrollTrigger} from 'gsap/ScrollTrigger';
|
import {ScrollTrigger} from 'gsap/ScrollTrigger';
|
||||||
@@ -30,9 +26,28 @@ export default function HorizontalShowcase() {
|
|||||||
const ctx = gsap.context(() => {
|
const ctx = gsap.context(() => {
|
||||||
const track = trackRef.current;
|
const track = trackRef.current;
|
||||||
const section = sectionRef.current;
|
const section = sectionRef.current;
|
||||||
|
|
||||||
if (!track || !section) return;
|
if (!track || !section) return;
|
||||||
|
|
||||||
const scrollDistance = () => track.scrollWidth - window.innerWidth;
|
const scrollDistance = () => (
|
||||||
|
Math.max(track.scrollWidth - window.innerWidth, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
let animationFrameId = null;
|
||||||
|
let latestProgress = 0;
|
||||||
|
|
||||||
|
const syncVideo = (progress) => {
|
||||||
|
latestProgress = progress;
|
||||||
|
if (animationFrameId !== null) return;
|
||||||
|
|
||||||
|
animationFrameId = window.requestAnimationFrame(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (video && Number.isFinite(video.duration) && video.duration > 0) {
|
||||||
|
video.currentTime = latestProgress * video.duration;
|
||||||
|
}
|
||||||
|
animationFrameId = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const horizontalTween = gsap.to(track, {
|
const horizontalTween = gsap.to(track, {
|
||||||
x: () => -scrollDistance(),
|
x: () => -scrollDistance(),
|
||||||
@@ -44,16 +59,12 @@ export default function HorizontalShowcase() {
|
|||||||
scrub: 1,
|
scrub: 1,
|
||||||
pin: true,
|
pin: true,
|
||||||
invalidateOnRefresh: true,
|
invalidateOnRefresh: true,
|
||||||
onUpdate: (self) => {
|
onUpdate: (self) => syncVideo(self.progress),
|
||||||
const video = videoRef.current;
|
|
||||||
if (video && video.duration) {
|
|
||||||
video.currentTime = self.progress * video.duration;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (animationFrameId !== null) window.cancelAnimationFrame(animationFrameId);
|
||||||
horizontalTween.scrollTrigger?.kill();
|
horizontalTween.scrollTrigger?.kill();
|
||||||
horizontalTween.kill();
|
horizontalTween.kill();
|
||||||
};
|
};
|
||||||
@@ -64,6 +75,7 @@ export default function HorizontalShowcase() {
|
|||||||
|
|
||||||
const handleModalAddToCart = () => {
|
const handleModalAddToCart = () => {
|
||||||
if (!quickViewMachine) return;
|
if (!quickViewMachine) return;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addToCart({
|
addToCart({
|
||||||
id: quickViewMachine.id,
|
id: quickViewMachine.id,
|
||||||
@@ -73,6 +85,7 @@ export default function HorizontalShowcase() {
|
|||||||
image: quickViewMachine.image,
|
image: quickViewMachine.image,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
message.success(t.cart.addToast(quickViewMachine.name));
|
message.success(t.cart.addToast(quickViewMachine.name));
|
||||||
setQuickViewMachine(null);
|
setQuickViewMachine(null);
|
||||||
};
|
};
|
||||||
@@ -115,12 +128,19 @@ export default function HorizontalShowcase() {
|
|||||||
>
|
>
|
||||||
{quickViewMachine && (
|
{quickViewMachine && (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<img src={quickViewMachine.image} alt={quickViewMachine.name}
|
<img
|
||||||
className="w-full h-64 object-contain"/>
|
src={quickViewMachine.image}
|
||||||
|
alt={quickViewMachine.name}
|
||||||
|
className="w-full h-64 object-contain"
|
||||||
|
/>
|
||||||
<p className="text-secondary leading-7">{quickViewMachine.description}</p>
|
<p className="text-secondary leading-7">{quickViewMachine.description}</p>
|
||||||
<p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p>
|
<p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p>
|
||||||
<Button type="primary" size="large" icon={<ShoppingCartOutlined/>}
|
<Button
|
||||||
onClick={handleModalAddToCart}>
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
icon={<ShoppingCartOutlined/>}
|
||||||
|
onClick={handleModalAddToCart}
|
||||||
|
>
|
||||||
{t.machineActions.addToCart}
|
{t.machineActions.addToCart}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,4 +148,4 @@ export default function HorizontalShowcase() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -95,8 +95,7 @@ export default function MapSection() {
|
|||||||
|
|
||||||
<div className="grid lg:grid-cols-[1.3fr,1fr] gap-6">
|
<div className="grid lg:grid-cols-[1.3fr,1fr] gap-6">
|
||||||
<div className="rounded-2xl overflow-hidden border border-subtle h-[360px] lg:h-auto min-h-[360px]">
|
<div className="rounded-2xl overflow-hidden border border-subtle h-[360px] lg:h-auto min-h-[360px]">
|
||||||
{/*<MapThumb coords={pins[0].coords} mapStyle={mapStyle} alt={pins[0].label} />*/}
|
<MapThumb coords={pins[0].coords} mapStyle={mapStyle} alt={pins[0].label}/>
|
||||||
<Image src="/images/map.png" alt="روبات مارکت" width={600} height={600} className={"object-contain"} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {useState} from 'react';
|
||||||
|
import {Modal, Button, message} from 'antd';
|
||||||
|
import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons';
|
||||||
|
import {useDispatch} from 'react-redux';
|
||||||
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
|
import {addToCart} from '@/store/cartSlice';
|
||||||
|
|
||||||
|
export default function MobileShowcase() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const {t, dir} = useLanguage();
|
||||||
|
const [quickViewMachine, setQuickViewMachine] = useState(null);
|
||||||
|
|
||||||
|
const handleAddToCart = (machine) => {
|
||||||
|
dispatch(
|
||||||
|
addToCart({
|
||||||
|
id: machine.id,
|
||||||
|
name: machine.name,
|
||||||
|
priceNumeric: machine.priceNumeric,
|
||||||
|
priceLabel: machine.priceLabel,
|
||||||
|
image: machine.image,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
message.success(t.cart.addToast(machine.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalAddToCart = () => {
|
||||||
|
if (!quickViewMachine) return;
|
||||||
|
handleAddToCart(quickViewMachine);
|
||||||
|
setQuickViewMachine(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="showcase" className="relative w-full py-8 px-4" dir={dir}>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{t.machines.map((machine) => (
|
||||||
|
<div
|
||||||
|
key={machine.id}
|
||||||
|
className="flex gap-4 items-center bg-surface border border-subtle rounded-2xl p-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={machine.image}
|
||||||
|
alt={machine.name}
|
||||||
|
className="w-24 h-24 object-contain shrink-0"
|
||||||
|
onClick={() => setQuickViewMachine(machine)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5 min-w-0 flex-1">
|
||||||
|
<span className="text-brand-400 text-xs font-semibold tracking-wide">
|
||||||
|
{machine.shortLabel}
|
||||||
|
</span>
|
||||||
|
<h3 className="text-lg font-bold text-primary truncate">
|
||||||
|
{machine.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-secondary line-clamp-2">
|
||||||
|
{machine.description}
|
||||||
|
</p>
|
||||||
|
<p className="text-base font-bold text-primary mt-1">
|
||||||
|
{machine.priceLabel}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-start items-center mt-3">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="middle"
|
||||||
|
icon={<ShoppingCartOutlined/>}
|
||||||
|
onClick={() => handleAddToCart(machine)}
|
||||||
|
>
|
||||||
|
{t.machineActions.addToCart}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={!!quickViewMachine}
|
||||||
|
onCancel={() => setQuickViewMachine(null)}
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
title={quickViewMachine?.name}
|
||||||
|
>
|
||||||
|
{quickViewMachine && (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<img
|
||||||
|
src={quickViewMachine.image}
|
||||||
|
alt={quickViewMachine.name}
|
||||||
|
className="w-full h-64 object-contain"
|
||||||
|
/>
|
||||||
|
<p className="text-secondary leading-7">{quickViewMachine.description}</p>
|
||||||
|
<p className="text-xl font-bold text-primary">{quickViewMachine.priceLabel}</p>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
icon={<ShoppingCartOutlined/>}
|
||||||
|
onClick={handleModalAddToCart}
|
||||||
|
>
|
||||||
|
{t.machineActions.addToCart}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import HorizontalShowcase from './HorizontalShowcase';
|
||||||
|
import MobileShowcase from './MobileShowcase';
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
export default function ShowcaseSection() {
|
||||||
|
const [isMobile, setIsMobile] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia(
|
||||||
|
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (event) => setIsMobile(event.matches);
|
||||||
|
|
||||||
|
setIsMobile(mediaQuery.matches);
|
||||||
|
mediaQuery.addEventListener('change', handleChange);
|
||||||
|
|
||||||
|
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isMobile === null) return null;
|
||||||
|
|
||||||
|
return isMobile ? <MobileShowcase/> : <HorizontalShowcase/>;
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// دکمه سوییچ حالت روشن/تیره.
|
|
||||||
|
|
||||||
import { Button, Tooltip } from 'antd';
|
import { Button, Tooltip } from 'antd';
|
||||||
import { SunOutlined, MoonOutlined } from '@ant-design/icons';
|
import { SunOutlined, MoonOutlined } from '@ant-design/icons';
|
||||||
|
|||||||
@@ -1,123 +1,115 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// کارت نمایش هر دستگاه داخل ویترین: شامل جلوه اسپاتلایت (هاور روی عکس دستگاه رنگ جایگزین را نشان میدهد)،
|
|
||||||
// آیتمهای شناور، گیف+توضیحات، و دکمههای افزودن به سبد / مشاهده سریع / علاقهمندی.
|
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import {useRef} from 'react';
|
||||||
import { Button, Tooltip, message } from 'antd';
|
import {Button, message} from 'antd';
|
||||||
import { ShoppingCartOutlined, EyeOutlined, HeartOutlined, HeartFilled } from '@ant-design/icons';
|
import {ShoppingCartOutlined, EyeOutlined} from '@ant-design/icons';
|
||||||
import { useDispatch } from 'react-redux';
|
import {useDispatch} from 'react-redux';
|
||||||
import { addToCart } from '@/store/cartSlice';
|
import {addToCart} from '@/store/cartSlice';
|
||||||
import { useLanguage } from '@/app/context/LanguageContext';
|
import {useLanguage} from '@/app/context/LanguageContext';
|
||||||
import FloatingItems from './FloatingItems';
|
import FloatingItems from './FloatingItems';
|
||||||
import { floatingSnacks } from '@/data/content';
|
import {floatingSnacks} from '@/data/content';
|
||||||
|
|
||||||
export default function VendingCard({ machine, onQuickView }) {
|
export default function VendingCard({machine, onQuickView}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { t, dir } = useLanguage();
|
const {t, dir} = useLanguage();
|
||||||
const [wishlisted, setWishlisted] = useState(false);
|
const stageRef = useRef(null);
|
||||||
const stageRef = useRef(null);
|
const lensRef = useRef(null);
|
||||||
const lensRef = useRef(null);
|
const revealRef = useRef(null);
|
||||||
const revealRef = useRef(null);
|
|
||||||
|
|
||||||
const handleMouseMove = (e) => {
|
const handleMouseMove = (e) => {
|
||||||
const stage = stageRef.current;
|
const stage = stageRef.current;
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
const rect = stage.getBoundingClientRect();
|
const rect = stage.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
const y = e.clientY - rect.top;
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
if (lensRef.current) {
|
if (lensRef.current) {
|
||||||
lensRef.current.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`;
|
lensRef.current.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`;
|
||||||
}
|
}
|
||||||
if (revealRef.current) {
|
if (revealRef.current) {
|
||||||
revealRef.current.style.clipPath = `circle(95px at ${x}px ${y}px)`;
|
revealRef.current.style.clipPath = `circle(95px at ${x}px ${y}px)`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
if (revealRef.current) revealRef.current.style.clipPath = 'circle(0px at 50% 50%)';
|
if (revealRef.current) revealRef.current.style.clipPath = 'circle(0px at 50% 50%)';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
addToCart({
|
addToCart({
|
||||||
id: machine.id,
|
id: machine.id,
|
||||||
name: machine.name,
|
name: machine.name,
|
||||||
priceNumeric: machine.priceNumeric,
|
priceNumeric: machine.priceNumeric,
|
||||||
priceLabel: machine.priceLabel,
|
priceLabel: machine.priceLabel,
|
||||||
image: machine.image,
|
image: machine.image,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
message.success(t.cart.addToast(machine.name));
|
message.success(t.cart.addToast(machine.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="horizontal-panel flex items-center justify-center px-6 md:px-16" dir={dir}>
|
<div className="horizontal-panel flex items-center justify-center px-6 md:px-16" dir={dir}>
|
||||||
<div className="relative z-10 max-w-6xl w-full grid md:grid-cols-2 gap-10 items-center">
|
<div className="relative z-10 max-w-6xl w-full grid md:grid-cols-2 gap-0 md:gap-10 items-center">
|
||||||
<div
|
<div
|
||||||
ref={stageRef}
|
ref={stageRef}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
className="spotlight-stage relative h-[420px] md:h-[560px] flex items-center justify-center"
|
className="spotlight-stage relative h-48 md:h-[500px] flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<FloatingItems items={floatingSnacks} />
|
<FloatingItems items={floatingSnacks}/>
|
||||||
|
|
||||||
<img src={machine.image} alt={machine.name} className="base-layer z-10" />
|
<img src={machine.image} alt={machine.name} className="base-layer z-10"/>
|
||||||
<img
|
<img
|
||||||
ref={revealRef}
|
ref={revealRef}
|
||||||
src={machine.imageAlt}
|
src={machine.imageAlt}
|
||||||
alt={`${machine.name} - ${t.machineActions.altVariant}`}
|
alt={`${machine.name} - ${t.machineActions.altVariant}`}
|
||||||
className="reveal-layer z-20"
|
className="reveal-layer z-20"
|
||||||
/>
|
/>
|
||||||
<div ref={lensRef} className="spotlight-lens" />
|
<div ref={lensRef} className="spotlight-lens"/>
|
||||||
|
|
||||||
<span className="absolute top-3 z-30 text-[11px] px-3 py-1 rounded-full bg-brand/15 border border-subtle text-brand-400 start-3">
|
<span
|
||||||
|
className="absolute hidden md:block top-3 z-30 text-[11px] px-3 py-1 rounded-full bg-brand/15 border border-subtle text-brand-400 start-3">
|
||||||
{t.machineActions.hoverHint}
|
{t.machineActions.hoverHint}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-center items-center gap-5 ">
|
||||||
|
<span className="text-brand-400 text-sm font-semibold tracking-wide">{machine.shortLabel}</span>
|
||||||
|
<h3 className="text-2xl md:text-4xl font-extrabold text-primary">{machine.name}</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-[auto,1fr] gap-4 items-start bg-surface border border-subtle rounded-2xl p-4">
|
||||||
|
<img
|
||||||
|
src={machine.gif}
|
||||||
|
alt={t.machineActions.gifAlt(machine.name)}
|
||||||
|
className="w-28 h-14 md:w-32 md:h-24 object-cover rounded-lg border border-subtle"
|
||||||
|
/>
|
||||||
|
<p className="text-sm md:text-base text-secondary md:leading-7">{machine.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="flex flex-wrap gap-2">
|
||||||
|
{machine.highlights.map((h) => (
|
||||||
|
<li key={h}
|
||||||
|
className="text-xs md:text-sm text-brand-400 bg-brand/10 border border-subtle rounded-full px-3 py-1">
|
||||||
|
{h}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p className="text-xl md:text-2xl font-bold text-primary mt-2">{machine.priceLabel}</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2 mt-2 ">
|
||||||
|
{/* <Button type="primary" size="large" icon={<ShoppingCartOutlined/>} onClick={handleAddToCart}>
|
||||||
|
{t.machineActions.addToCart}
|
||||||
|
</Button>*/}
|
||||||
|
<Button size="large" icon={<EyeOutlined/>} onClick={() => onQuickView?.(machine)}>
|
||||||
|
{t.machineActions.quickView}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<div className="flex flex-col gap-5">
|
|
||||||
<span className="text-brand-400 text-sm font-semibold tracking-wide">{machine.shortLabel}</span>
|
|
||||||
<h3 className="text-2xl md:text-4xl font-extrabold text-primary">{machine.name}</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-[auto,1fr] gap-4 items-start bg-surface border border-subtle rounded-2xl p-4">
|
|
||||||
<img
|
|
||||||
src={machine.gif}
|
|
||||||
alt={t.machineActions.gifAlt(machine.name)}
|
|
||||||
className="w-28 h-20 md:w-32 md:h-24 object-cover rounded-lg border border-subtle"
|
|
||||||
/>
|
|
||||||
<p className="text-sm md:text-base text-secondary leading-7">{machine.description}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="flex flex-wrap gap-2">
|
|
||||||
{machine.highlights.map((h) => (
|
|
||||||
<li key={h} className="text-xs md:text-sm text-brand-400 bg-brand/10 border border-subtle rounded-full px-3 py-1">
|
|
||||||
{h}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p className="text-xl md:text-2xl font-bold text-primary mt-2">{machine.priceLabel}</p>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3 mt-2">
|
|
||||||
<Button type="primary" size="large" icon={<ShoppingCartOutlined />} onClick={handleAddToCart}>
|
|
||||||
{t.machineActions.addToCart}
|
|
||||||
</Button>
|
|
||||||
<Button size="large" icon={<EyeOutlined />} onClick={() => onQuickView?.(machine)}>
|
|
||||||
{t.machineActions.quickView}
|
|
||||||
</Button>
|
|
||||||
<Tooltip title={wishlisted ? t.machineActions.wishlistRemove : t.machineActions.wishlistAdd}>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
shape="circle"
|
|
||||||
icon={wishlisted ? <HeartFilled className="text-brand-400" /> : <HeartOutlined />}
|
|
||||||
onClick={() => setWishlisted((v) => !v)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,36 +9,36 @@ const machinesFa = [
|
|||||||
id: 'rm-35',
|
id: 'rm-35',
|
||||||
name: 'روبات مارکت ۳۵ کانال',
|
name: 'روبات مارکت ۳۵ کانال',
|
||||||
shortLabel: '۳۵ کانال',
|
shortLabel: '۳۵ کانال',
|
||||||
priceLabel: '۳,۸۶۰,۰۰۰,۰۰۰ ریال',
|
priceLabel: '4,200,000,000 ریال',
|
||||||
priceNumeric: 3860000000,
|
priceNumeric: 4200000000,
|
||||||
description: 'یخچالدار، ۳۵ کانال اختصاصی، سیستم خنککننده قدرتمند، تحویل نرم، مدیریت آنلاین ۲۴ ساعته.',
|
description: 'یخچالدار، ۳۵ کانال اختصاصی، سیستم خنککننده قدرتمند، تحویل نرم، مدیریت آنلاین ۲۴ ساعته.',
|
||||||
highlights: ['یخچالدار', '۳۵ کانال اختصاصی', 'تحویل نرم', 'مدیریت آنلاین ۲۴ ساعته'],
|
highlights: ['یخچالدار', '۳۵ کانال اختصاصی', 'تحویل نرم', 'مدیریت آنلاین ۲۴ ساعته'],
|
||||||
image: '/images/machine-35-base.png',
|
image: '/images/machine-35-base.webp',
|
||||||
imageAlt: '/images/machine-35-white.png',
|
imageAlt: '/images/machine-35-white.webp',
|
||||||
gif: '/gifs/machine-35.gif',
|
gif: '/gifs/machine-35.gif',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rm-48',
|
id: 'rm-48',
|
||||||
name: 'روبات مارکت ۴۸ کانال',
|
name: 'روبات مارکت ۴۸ کانال',
|
||||||
shortLabel: '۴۸ کانال',
|
shortLabel: '۴۸ کانال',
|
||||||
priceLabel: '۴,۴۸۰,۰۰۰,۰۰۰ ریال',
|
priceLabel: '4,980,000,000 ریال',
|
||||||
priceNumeric: 4480000000,
|
priceNumeric: 4980000000,
|
||||||
description: 'یخچالدار، ۴۸ کانال اختصاصی، قابلیتهای پیشرفته مدیریت آنلاین و تحویل کالا.',
|
description: 'یخچالدار، ۴۸ کانال اختصاصی، قابلیتهای پیشرفته مدیریت آنلاین و تحویل کالا.',
|
||||||
highlights: ['یخچالدار', '۴۸ کانال اختصاصی', 'مدیریت آنلاین پیشرفته', 'تحویل هوشمند کالا'],
|
highlights: ['یخچالدار', '۴۸ کانال اختصاصی', 'مدیریت آنلاین پیشرفته', 'تحویل هوشمند کالا'],
|
||||||
image: '/images/machine-48-base.png',
|
image: '/images/machine-48-base.webp',
|
||||||
imageAlt: '/images/machine-48-white.png',
|
imageAlt: '/images/machine-48-white.webp',
|
||||||
gif: '/gifs/machine-48.gif',
|
gif: '/gifs/machine-48.gif',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rm-60',
|
id: 'rm-60',
|
||||||
name: 'روبات مارکت ۶۰ کانال',
|
name: 'روبات مارکت ۶۰ کانال',
|
||||||
shortLabel: '۶۰ کانال',
|
shortLabel: '۶۰ کانال',
|
||||||
priceLabel: '۴,۹۸۰,۰۰۰,۰۰۰ ریال',
|
priceLabel: '5,480,000,000 ریال',
|
||||||
priceNumeric: 4980000000,
|
priceNumeric: 5480000000,
|
||||||
description: 'یخچالدار، ۶۰ کانال اختصاصی برای کسبوکارهای پرتردد.',
|
description: 'یخچالدار، ۶۰ کانال اختصاصی برای کسبوکارهای پرتردد.',
|
||||||
highlights: ['یخچالدار', '۶۰ کانال اختصاصی', 'مناسب اماکن پرتردد', 'ظرفیت بالا'],
|
highlights: ['یخچالدار', '۶۰ کانال اختصاصی', 'مناسب اماکن پرتردد', 'ظرفیت بالا'],
|
||||||
image: '/images/machine-60-base.png',
|
image: '/images/machine-60-base.webp',
|
||||||
imageAlt: '/images/machine-60-white.png',
|
imageAlt: '/images/machine-60-white.webp',
|
||||||
gif: '/gifs/machine-60.gif',
|
gif: '/gifs/machine-60.gif',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -48,36 +48,36 @@ const machinesEn = [
|
|||||||
id: 'rm-35',
|
id: 'rm-35',
|
||||||
name: 'Robot Market RM-35',
|
name: 'Robot Market RM-35',
|
||||||
shortLabel: '35 Channels',
|
shortLabel: '35 Channels',
|
||||||
priceLabel: '3,860,000,000 Rials',
|
priceLabel: '4,180,000,000 Rials',
|
||||||
priceNumeric: 3860000000,
|
priceNumeric: 4180000000,
|
||||||
description: 'Refrigerated, 35 dedicated channels, powerful cooling system, soft delivery, 24/7 online management.',
|
description: 'Refrigerated, 35 dedicated channels, powerful cooling system, soft delivery, 24/7 online management.',
|
||||||
highlights: ['Refrigerated', '35 dedicated channels', 'Soft delivery', '24/7 online management'],
|
highlights: ['Refrigerated', '35 dedicated channels', 'Soft delivery', '24/7 online management'],
|
||||||
image: '/images/machine-35-base.png',
|
image: '/images/machine-35-base.webp',
|
||||||
imageAlt: '/images/machine-35-white.png',
|
imageAlt: '/images/machine-35-white.webp',
|
||||||
gif: '/gifs/machine-35.gif',
|
gif: '/gifs/machine-35.gif',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rm-48',
|
id: 'rm-48',
|
||||||
name: 'Robot Market RM-48',
|
name: 'Robot Market RM-48',
|
||||||
shortLabel: '48 Channels',
|
shortLabel: '48 Channels',
|
||||||
priceLabel: '4,480,000,000 Rials',
|
priceLabel: '4,980,000,000 Rials',
|
||||||
priceNumeric: 4480000000,
|
priceNumeric: 4980000000,
|
||||||
description: 'Refrigerated, 48 dedicated channels, advanced online management and delivery capabilities.',
|
description: 'Refrigerated, 48 dedicated channels, advanced online management and delivery capabilities.',
|
||||||
highlights: ['Refrigerated', '48 dedicated channels', 'Advanced online management', 'Smart delivery'],
|
highlights: ['Refrigerated', '48 dedicated channels', 'Advanced online management', 'Smart delivery'],
|
||||||
image: '/images/machine-48-base.png',
|
image: '/images/machine-48-base.webp',
|
||||||
imageAlt: '/images/machine-48-white.png',
|
imageAlt: '/images/machine-48-white.webp',
|
||||||
gif: '/gifs/machine-48.gif',
|
gif: '/gifs/machine-48.gif',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rm-60',
|
id: 'rm-60',
|
||||||
name: 'Robot Market RM-60',
|
name: 'Robot Market RM-60',
|
||||||
shortLabel: '60 Channels',
|
shortLabel: '60 Channels',
|
||||||
priceLabel: '4,980,000,000 Rials',
|
priceLabel: '5,480,000,000 Rials',
|
||||||
priceNumeric: 4980000000,
|
priceNumeric: 5480000000,
|
||||||
description: 'Refrigerated, 60 dedicated channels for high-traffic businesses.',
|
description: 'Refrigerated, 60 dedicated channels for high-traffic businesses.',
|
||||||
highlights: ['Refrigerated', '60 dedicated channels', 'Ideal for high-traffic venues', 'High capacity'],
|
highlights: ['Refrigerated', '60 dedicated channels', 'Ideal for high-traffic venues', 'High capacity'],
|
||||||
image: '/images/machine-60-base.png',
|
image: '/images/machine-60-base.webp',
|
||||||
imageAlt: '/images/machine-60-white.png',
|
imageAlt: '/images/machine-60-white.webp',
|
||||||
gif: '/gifs/machine-60.gif',
|
gif: '/gifs/machine-60.gif',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -112,21 +112,21 @@ export const content = {
|
|||||||
kicker: 'روبات مارکت',
|
kicker: 'روبات مارکت',
|
||||||
titleLines: ['فروش خودکار،', 'هوشمند و بیوقفه'],
|
titleLines: ['فروش خودکار،', 'هوشمند و بیوقفه'],
|
||||||
sub: 'تجربهای نوین از خرید آسان و سریع با دستگاههای وندینگ روبات مارکت.',
|
sub: 'تجربهای نوین از خرید آسان و سریع با دستگاههای وندینگ روبات مارکت.',
|
||||||
image: '/images/machine-slide-0.png',
|
image: '/images/machine-slide-0.webp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'slide-2',
|
id: 'slide-2',
|
||||||
kicker: 'یخچالدار',
|
kicker: 'یخچالدار',
|
||||||
titleLines: ['خنککننده قدرتمند،', 'کیفیت تضمینی'],
|
titleLines: ['خنککننده قدرتمند،', 'کیفیت تضمینی'],
|
||||||
sub: 'نگهداری اصولی تنقلات و نوشیدنی سرد، ۲۴ ساعته و بدون نیاز به نیروی انسانی.',
|
sub: 'نگهداری اصولی تنقلات و نوشیدنی سرد، ۲۴ ساعته و بدون نیاز به نیروی انسانی.',
|
||||||
image: '/images/machine-slide-1.png',
|
image: '/images/machine-slide-1.webp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'slide-3',
|
id: 'slide-3',
|
||||||
kicker: 'مدیریت آنلاین',
|
kicker: 'مدیریت آنلاین',
|
||||||
titleLines: ['کنترل کامل کسبوکار', 'از راه دور'],
|
titleLines: ['کنترل کامل کسبوکار', 'از راه دور'],
|
||||||
sub: 'مدیریت موجودی، فروش و گزارشگیری آنلاین، هر ساعت از شبانهروز.',
|
sub: 'مدیریت موجودی، فروش و گزارشگیری آنلاین، هر ساعت از شبانهروز.',
|
||||||
image: '/images/machine-slide-2.png',
|
image: '/images/machine-slide-2.webp',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -402,21 +402,21 @@ export const content = {
|
|||||||
kicker: 'Robot Market',
|
kicker: 'Robot Market',
|
||||||
titleLines: ['Automated selling,', 'smart & non-stop'],
|
titleLines: ['Automated selling,', 'smart & non-stop'],
|
||||||
sub: 'A new experience of fast, easy shopping with Robot Market vending machines.',
|
sub: 'A new experience of fast, easy shopping with Robot Market vending machines.',
|
||||||
image: '/images/machine-slide-0.png',
|
image: '/images/machine-slide-0.webp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'slide-2',
|
id: 'slide-2',
|
||||||
kicker: 'Refrigerated',
|
kicker: 'Refrigerated',
|
||||||
titleLines: ['Powerful cooling,', 'guaranteed quality'],
|
titleLines: ['Powerful cooling,', 'guaranteed quality'],
|
||||||
sub: 'Proper storage for snacks and cold drinks, 24/7, with no staff required.',
|
sub: 'Proper storage for snacks and cold drinks, 24/7, with no staff required.',
|
||||||
image: '/images/machine-slide-1.png',
|
image: '/images/machine-slide-1.webp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'slide-3',
|
id: 'slide-3',
|
||||||
kicker: 'Online Management',
|
kicker: 'Online Management',
|
||||||
titleLines: ['Full control of your', 'business, remotely'],
|
titleLines: ['Full control of your', 'business, remotely'],
|
||||||
sub: 'Manage inventory, sales, and reports online, any hour of the day.',
|
sub: 'Manage inventory, sales, and reports online, any hour of the day.',
|
||||||
image: '/images/machine-slide-2.png',
|
image: '/images/machine-slide-2.webp',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -667,12 +667,12 @@ export const content = {
|
|||||||
|
|
||||||
// Locale-independent asset config (positions/paths only — same in both languages).
|
// Locale-independent asset config (positions/paths only — same in both languages).
|
||||||
export const floatingSnacks = [
|
export const floatingSnacks = [
|
||||||
{id: 'chips-1', image: '/images/snacks/item-1.png', size: 125, top: '5%', left: '-5%', duration: 5.5, delay: 0},
|
{id: 'chips-1', image: '/images/snacks/item-1.webp', size: 60, top: '5%', left: '-5%', duration: 5.5, delay: 0},
|
||||||
{id: 'soda-1', image: '/images/snacks/item-2.png', size: 85, top: '68%', left: '2%', duration: 6.2, delay: 0.6},
|
{id: 'soda-1', image: '/images/snacks/item-2.webp', size: 55, top: '68%', left: '2%', duration: 6.2, delay: 0.6},
|
||||||
{id: 'chips-2', image: '/images/snacks/item-3.png', size: 95, top: '10%', left: '85%', duration: 4.8, delay: 0.3},
|
{id: 'chips-2', image: '/images/snacks/item-3.webp', size: 65, top: '10%', left: '85%', duration: 4.8, delay: 0.3},
|
||||||
{id: 'soda-2', image: '/images/snacks/item-4.png', size: 105, top: '72%', left: '88%', duration: 5.9, delay: 0.9},
|
{id: 'soda-2', image: '/images/snacks/item-4.webp', size: 55, top: '72%', left: '88%', duration: 5.9, delay: 0.9},
|
||||||
{id: 'chips-3', image: '/images/snacks/item-5.png', size: 90, top: '42%', left: '90%', duration: 6.6, delay: 1.2},
|
{id: 'chips-3', image: '/images/snacks/item-5.webp', size: 50, top: '42%', left: '90%', duration: 6.6, delay: 1.2},
|
||||||
{id: 'soda-3', image: '/images/snacks/item-6.png', size: 95, top: '46%', left: '-4%', duration: 5.1, delay: 1.5},
|
{id: 'soda-3', image: '/images/snacks/item-6.webp', size: 55, top: '46%', left: '-4%', duration: 5.1, delay: 1.5},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const showcaseVideoSrc = '/video/showcase-placeholder.mp4';
|
export const showcaseVideoSrc = '/video/showcase-placeholder.mp4';
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
robot-market:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: robot-market
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[{"eventName":"NEXT_CLI_SESSION_STOPPED","payload":{"nextVersion":"14.2.5","nodeVersion":"v26.3.0","cliCommand":"dev","durationMilliseconds":263824,"turboFlag":false,"pagesDir":false,"appDir":true}}]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"pages": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"polyfillFiles": [
|
||||||
|
"static/chunks/polyfills.js"
|
||||||
|
],
|
||||||
|
"devFiles": [],
|
||||||
|
"ampDevFiles": [],
|
||||||
|
"lowPriorityFiles": [
|
||||||
|
"static/development/_buildManifest.js",
|
||||||
|
"static/development/_ssgManifest.js"
|
||||||
|
],
|
||||||
|
"rootMainFiles": [],
|
||||||
|
"pages": {
|
||||||
|
"/_app": []
|
||||||
|
},
|
||||||
|
"ampFirstPages": []
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"type": "commonjs"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
self.__BUILD_MANIFEST={"polyfillFiles":["static/chunks/polyfills.js"],"devFiles":[],"ampDevFiles":[],"lowPriorityFiles":["static/development/_buildManifest.js","static/development/_ssgManifest.js"],"rootMainFiles":[],"pages":{"/_app":[]},"ampFirstPages":[]}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"middleware": {},
|
||||||
|
"functions": {},
|
||||||
|
"sortedMiddleware": []
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
self.__REACT_LOADABLE_MANIFEST="{}"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"OV7lo+xPvS/sDjLEddseUuhg7MfbZYxNg/IFGVNi66w=\"\n}"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"node": {},
|
||||||
|
"edge": {},
|
||||||
|
"encryptionKey": "OV7lo+xPvS/sDjLEddseUuhg7MfbZYxNg/IFGVNi66w="
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},sortedPages:["\u002F_app"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"type": "module"}
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
// AntD ships CJS + uses some patterns that benefit from transpilation in the App Router.
|
|
||||||
transpilePackages: ['antd', '@ant-design/icons', 'rc-util', 'rc-pagination', 'rc-picker'],
|
transpilePackages: [
|
||||||
|
'antd',
|
||||||
|
'@ant-design/icons',
|
||||||
|
'rc-util',
|
||||||
|
'rc-pagination',
|
||||||
|
'rc-picker'
|
||||||
|
],
|
||||||
|
|
||||||
|
output: 'standalone',
|
||||||
|
|
||||||
|
trailingSlash: true,
|
||||||
|
|
||||||
|
basePath: basePath || undefined,
|
||||||
|
assetPrefix: basePath || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev -H 0.0.0.0",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 821 KiB |
|
Before Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 232 KiB |