Compare commits
3 Commits
0bd480d103
...
source-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7aa7c2013 | ||
|
|
4820289252 | ||
|
|
b7931731f9 |
@@ -1,97 +0,0 @@
|
||||
name: Build & Deploy to EasyPanel
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --no-audit --no-fund
|
||||
|
||||
- name: Build Astro site
|
||||
run: npm run build
|
||||
|
||||
- name: Trigger EasyPanel redeploy
|
||||
if: success()
|
||||
env:
|
||||
EASYPANEL_TOKEN: ${{ secrets.EASYPANEL_TOKEN }}
|
||||
EASYPANEL_PROJECT_NAME: ${{ secrets.EASYPANEL_PROJECT_NAME }}
|
||||
EASYPANEL_SERVICE_NAME: ${{ secrets.EASYPANEL_SERVICE_NAME }}
|
||||
run: |
|
||||
# EasyPanel tRPC endpoint for app service redeploy
|
||||
EASYPANEL_API="https://panelwebsite.moreminimore.com/api"
|
||||
DEPLOY_URL="${EASYPANEL_API}/trpc/services.app.deployService"
|
||||
|
||||
# Guard: required secrets
|
||||
if [ -z "$EASYPANEL_TOKEN" ] || [ -z "$EASYPANEL_PROJECT_NAME" ] || [ -z "$EASYPANEL_SERVICE_NAME" ]; then
|
||||
echo "::warning::One or more required secrets are empty:"
|
||||
[ -z "$EASYPANEL_TOKEN" ] && echo " - EASYPANEL_TOKEN"
|
||||
[ -z "$EASYPANEL_PROJECT_NAME" ] && echo " - EASYPANEL_PROJECT_NAME"
|
||||
[ -z "$EASYPANEL_SERVICE_NAME" ] && echo " - EASYPANEL_SERVICE_NAME"
|
||||
echo "Skipping deploy trigger. Set these in repo settings to enable auto-deploy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# tRPC mutation payload: {"json":{"projectName":"...","serviceName":"..."}}
|
||||
PAYLOAD=$(jq -nc \
|
||||
--arg pj "$EASYPANEL_PROJECT_NAME" \
|
||||
--arg sv "$EASYPANEL_SERVICE_NAME" \
|
||||
'{json:{projectName:$pj, serviceName:$sv}}')
|
||||
|
||||
echo "Triggering EasyPanel redeploy"
|
||||
echo " Endpoint: $DEPLOY_URL"
|
||||
echo " Project: $EASYPANEL_PROJECT_NAME"
|
||||
echo " Service: $EASYPANEL_SERVICE_NAME"
|
||||
echo " Payload: $PAYLOAD"
|
||||
echo ""
|
||||
|
||||
HTTP_CODE=$(curl -sS -o /tmp/easypanel-response.json -w "%{http_code}" \
|
||||
-X POST "$DEPLOY_URL" \
|
||||
-H "Authorization: Bearer *** \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
RESPONSE=$(cat /tmp/easypanel-response.json)
|
||||
echo " HTTP $HTTP_CODE"
|
||||
echo " Response: $RESPONSE"
|
||||
|
||||
# 2xx = success
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo ""
|
||||
echo "EasyPanel redeploy triggered successfully"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 4xx with explicit field errors = workflow is right, payload is wrong
|
||||
if [ "$HTTP_CODE" -ge 400 ] && [ "$HTTP_CODE" -lt 500 ]; then
|
||||
echo "::error::EasyPanel rejected the deploy request (HTTP $HTTP_CODE). See response above."
|
||||
echo "::error::Likely the payload shape does not match what the panel expects."
|
||||
echo "::error::Check https://panelwebsite.moreminimore.com/api for the procedure spec."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5xx = server error
|
||||
echo "::error::EasyPanel API error (HTTP $HTTP_CODE). Response: $RESPONSE"
|
||||
exit 1
|
||||
|
||||
- name: Upload build artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
retention-days: 7
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Lint & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
- run: npm ci --no-audit --no-fund
|
||||
- name: Build (catches syntax + missing-image errors before deploy)
|
||||
run: npm run build
|
||||
160
docs/ci-setup.md
160
docs/ci-setup.md
@@ -1,99 +1,109 @@
|
||||
# CI/CD Setup — EasyPanel Deploy
|
||||
# CI/CD Setup — Deploy via Gitea Webhook
|
||||
|
||||
Push to `main` triggers `build-and-deploy.yml`:
|
||||
1. Builds the Astro static site into `dist/`
|
||||
2. Uploads `dist/` as a 7-day artifact
|
||||
3. Calls EasyPanel tRPC endpoint to trigger a redeploy
|
||||
The site auto-rebuilds on every push to `main` via a Gitea **webhook**
|
||||
(no Actions runner, no `.gitea/workflows/`, no act_runner required).
|
||||
|
||||
## Required Gitea repo secrets
|
||||
## How it works
|
||||
|
||||
Go to **Settings → Actions → Secrets** and add three secrets:
|
||||
```
|
||||
git push origin main
|
||||
│
|
||||
▼
|
||||
Gitea Webhook (Gitea built-in, not Gitea Actions)
|
||||
│ POST Content-Type: application/json
|
||||
▼
|
||||
http://110.164.146.47:3000/api/deploy/<token>
|
||||
│ HTTP 200 "Deploying..."
|
||||
▼
|
||||
EasyPanel pulls repo, builds with Dockerfile, redeploys
|
||||
```
|
||||
|
||||
| Name | Value (for this site) | Where to get it |
|
||||
|---|---|---|
|
||||
| `EASYPANEL_TOKEN` | `cmq61xwrv000407qn9e2hhfuw` | EasyPanel → profile/settings → API tokens |
|
||||
| `EASYPANEL_PROJECT_NAME` | `customerwebsite` | EasyPanel → project name in the dashboard |
|
||||
| `EASYPANEL_SERVICE_NAME` | `dealplustech-astro` | EasyPanel → service name inside the project |
|
||||
## One-time webhook setup
|
||||
|
||||
> ⚠️ Project name ≠ repo name. The site lives in the `customerwebsite`
|
||||
> project on the panel; the repo is `dealplustech-astroreal`.
|
||||
In `https://git.moreminimore.com/kunthawat/dealplustech-astroreal/settings/hooks`:
|
||||
|
||||
If any of these are empty, the workflow logs a warning and skips the deploy
|
||||
trigger. The build still runs and the artifact is still uploaded.
|
||||
1. Click **Add Webhook → Gitea**
|
||||
2. Fill in:
|
||||
- **Payload URL**: `http://110.164.146.47:3000/api/deploy/772d2c3a4a7d8671657c947c059bc1cdc64bd816efb7fbe2`
|
||||
- **HTTP Method**: `POST`
|
||||
- **Content Type**: `application/json`
|
||||
- **Events**: **Push events**
|
||||
- **Active**: ✓
|
||||
- **Branch filter**: leave empty (or `main` to restrict)
|
||||
3. Click **Add Webhook**
|
||||
4. Test: click the webhook row → **Test Delivery** → **Push events** → confirm
|
||||
**Last Response** is HTTP 200.
|
||||
|
||||
Done. From now on every push to main redeploys automatically.
|
||||
|
||||
## EasyPanel service requirements
|
||||
|
||||
The service on the panel side must be:
|
||||
The service on the panel side (`project=customerwebsite`,
|
||||
`service=dealplustech-astro`) must be:
|
||||
|
||||
- **Type: `app`**
|
||||
- **Source: Git**, pointing at this repo (`kunthawat/dealplustech-astroreal`)
|
||||
on branch **`main`** (not `source-code`).
|
||||
- **Build type: `dockerfile`**, **file: `Dockerfile`** at the repo root.
|
||||
> The workflow used to trigger nixpacks builds, but nixpacks expects a
|
||||
> `start` script in `package.json` and an Astro static site doesn't
|
||||
> have one. Switching to a Dockerfile + nginx fixes that.
|
||||
- **Source: Git**, branch `main`
|
||||
- **Build type: `dockerfile`**, file `Dockerfile` at repo root
|
||||
- **Port: `80`**
|
||||
|
||||
If you need a different service type later, swap the endpoint in
|
||||
`.gitea/workflows/build-and-deploy.yml` to the matching procedure:
|
||||
- `services.app.deployService` (Dockerfile / app)
|
||||
- `services.box.rebuildDockerImage` (low-level)
|
||||
- `services.compose.deployService` (docker-compose)
|
||||
The Dockerfile is two-stage:
|
||||
|
||||
## Why Dockerfile and not nixpacks
|
||||
1. `node:22-alpine` — runs `npm ci` and `npm run build` to produce
|
||||
`dist/`
|
||||
2. `nginx:1.27-alpine` — copies `dist/` to nginx's web root and serves
|
||||
it (with gzip, security headers, 1-year cache for hashed assets,
|
||||
and a `try_files` fallback for Astro's UTF-8 slugs)
|
||||
|
||||
Astro builds to static files in `dist/` — there is no Node server to run.
|
||||
Nixpacks tries to find a `start` command and fails. The Dockerfile in
|
||||
this repo:
|
||||
## Why not Gitea Actions?
|
||||
|
||||
1. **Stage 1** (`node:20-alpine`): runs `npm ci` then `npm run build` to
|
||||
produce `dist/`.
|
||||
2. **Stage 2** (`nginx:1.27-alpine`): copies `dist/` to nginx's web root
|
||||
and serves it.
|
||||
Gitea Actions does **not** ship with managed runners (unlike GitHub
|
||||
Actions' `ubuntu-latest`). Without a self-hosted `act_runner` registered
|
||||
with matching labels, any workflow run will fail with
|
||||
**"No matching online runner with label: ubuntu-latest"**.
|
||||
|
||||
`nginx.conf` ships gzip, security headers, 1-year cache for hashed
|
||||
assets, and a `try_files` fallback for client-side routes (Astro's
|
||||
file-based routing produces UTF-8 slugs).
|
||||
The Gitea Webhook mechanism is built into Gitea itself — no runner
|
||||
required. It fires the same events but POSTs to a URL you control,
|
||||
which is the simplest possible deploy trigger.
|
||||
|
||||
## Verifying the trigger payload
|
||||
If a real CI step is ever needed (lint, test, build artifact), install
|
||||
`act_runner` on a box, register it, and write a workflow using
|
||||
`runs-on: self-hosted`. See `act_runner` docs for setup.
|
||||
|
||||
If the deploy runs but the panel rejects it, the response in the workflow
|
||||
log will show the exact `zodErrors` field telling you which field name
|
||||
or shape is wrong. Update the `PAYLOAD` JSON in the workflow accordingly.
|
||||
## Troubleshooting
|
||||
|
||||
To test the payload shape from your local machine:
|
||||
### Push happened, but site didn't update
|
||||
|
||||
1. Open the webhook row in Settings → Webhooks
|
||||
2. Open **Recent Deliveries**
|
||||
3. Check the most recent one:
|
||||
- Status 200 → deploy endpoint received it, check EasyPanel logs
|
||||
- Status != 200 → check the response body
|
||||
- **No recent delivery** → webhook is not bound to this branch/event
|
||||
or the push target branch doesn't match the filter
|
||||
|
||||
### Build fails inside EasyPanel
|
||||
|
||||
1. EasyPanel UI → service `dealplustech-astro` → Logs tab
|
||||
2. Look for the `nixpacks` or `docker build` step
|
||||
3. Common failures:
|
||||
- **"No start command could be found"**: you're on `nixpacks`. Switch
|
||||
build type to `dockerfile` and point at `Dockerfile` at repo root.
|
||||
- **"Node.js v20.x is not supported by Astro"**: the Dockerfile is
|
||||
using `node:20-alpine`. It should be `node:22-alpine` (Astro 6
|
||||
requires `>=22.12.0`).
|
||||
- **"Failed to sync changes"** (HTTP 500 from the deploy endpoint):
|
||||
usually a panel-side state issue. Retry, or open the service in
|
||||
the panel UI and check container state.
|
||||
|
||||
### Want to redeploy without a code change
|
||||
|
||||
The webhook URL itself is idempotent. You can hit it directly with:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
"https://panelwebsite.moreminimore.com/api/trpc/services.app.deployService" \
|
||||
-H "Authorization: Bearer *** -H "Content-Type: application/json" \
|
||||
-d '{"json":{"projectName":"customerwebsite","serviceName":"dealplustech-astro"}}'
|
||||
curl -X POST \
|
||||
"http://110.164.146.47:3000/api/deploy/772d2c3a4a7d8671657c947c059bc1cdc64bd816efb7fbe2"
|
||||
```
|
||||
|
||||
A 2xx response = the panel accepted the trigger. The service will start
|
||||
rebuilding/redploying in the EasyPanel dashboard.
|
||||
|
||||
## Common failure: "No start command could be found" (nixpacks)
|
||||
|
||||
Astro builds to static files in `dist/` — there is no Node server to run.
|
||||
Nixpacks tries to find a `start` command and fails. The Dockerfile in
|
||||
this repo handles this with a two-stage build (node:22-alpine build +
|
||||
nginx:1.27-alpine serve).
|
||||
|
||||
## Common failure: "Node.js v20.x is not supported by Astro"
|
||||
|
||||
Astro 6 requires Node `>=22.12.0`. The Dockerfile uses `node:22-alpine`.
|
||||
If you see this error, the panel is probably reading a stale `node:20`
|
||||
cached image. Push an empty commit to force a rebuild, or clear the
|
||||
panel's image cache from the EasyPanel UI.
|
||||
|
||||
## Common failure: "Failed to sync changes" (HTTP 500)
|
||||
|
||||
This is a **server-side** error, not a payload problem. It usually means:
|
||||
|
||||
- The EasyPanel service is currently in a state that can't be redeployed
|
||||
(e.g. the previous container is stuck, or a build is in progress).
|
||||
- The Docker daemon on the panel host is busy.
|
||||
|
||||
Try again in a few seconds. If it persists, open the EasyPanel dashboard
|
||||
and check the service's container state.
|
||||
A `200` response with body `Deploying...` means the panel accepted the
|
||||
trigger. The actual rebuild happens in the background — check EasyPanel
|
||||
UI for the progress.
|
||||
|
||||
@@ -430,9 +430,115 @@ const productLinks = [
|
||||
<slot />
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-neutral-800 bg-gradient-to-b from-white to-neutral-50" data-animate="fade-up">
|
||||
<footer class="text-neutral-800 bg-gradient-to-b from-white to-neutral-50 overflow-x-clip" data-animate="fade-up">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Mobile layout (<1024px): 3 rows, all centered, content kept -->
|
||||
<!-- narrower than the container so long rows (especially the -->
|
||||
<!-- button row) do not stretch edge to edge. -->
|
||||
<!-- `overflow-x-clip` on <footer> catches the initial state of -->
|
||||
<!-- data-animate="fade-left"/"fade-right" children which are -->
|
||||
<!-- translated off-screen until the IntersectionObserver fires. -->
|
||||
<!-- ============================================================ -->
|
||||
<div class="lg:hidden space-y-10 text-center">
|
||||
<!-- Row 1: Company Info -->
|
||||
<div class="max-w-md mx-auto" data-animate="fade-up">
|
||||
<img src="/images/logo/dealplustech-logo.png" alt="ดีล พลัส เทค" class="h-10 w-auto mb-4 mx-auto transition-transform hover:scale-105 duration-300" />
|
||||
<div class="space-y-3 text-sm text-neutral-600 inline-flex flex-col items-center">
|
||||
<p class="flex items-start gap-2 hover:text-primary-600 transition-colors">
|
||||
<svg class="w-5 h-5 shrink-0 mt-0.5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span>{companyInfo.address}</span>
|
||||
</p>
|
||||
<p class="flex items-center gap-2 hover:text-primary-600 transition-colors">
|
||||
<svg class="w-5 h-5 shrink-0 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<a href={`tel:${companyInfo.phone}`}>{companyInfo.phone}</a>
|
||||
</p>
|
||||
<p class="flex items-center gap-2 hover:text-primary-600 transition-colors">
|
||||
<svg class="w-5 h-5 shrink-0 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<a href={`mailto:${companyInfo.email}`}>{companyInfo.email}</a>
|
||||
</p>
|
||||
<p class="flex items-center gap-2 hover:text-primary-600 transition-colors">
|
||||
<svg class="w-5 h-5 shrink-0 text-primary-600" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"/>
|
||||
</svg>
|
||||
<span>LINE: {companyInfo.line}</span>
|
||||
</p>
|
||||
<p class="text-neutral-400 text-xs">{companyInfo.hours}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Quick Links + Products (2 columns) -->
|
||||
<div class="grid grid-cols-2 gap-6 text-center max-w-2xl mx-auto">
|
||||
<div data-animate="fade-up" data-animate-delay="100">
|
||||
<h3 class="font-semibold text-lg mb-4 text-primary-700">ลิงก์ด่วน</h3>
|
||||
<ul class="space-y-2 text-sm text-neutral-600">
|
||||
<li><a href="/" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">หน้าแรก</a></li>
|
||||
<li><a href="/about-us" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">เกี่ยวกับเรา</a></li>
|
||||
<li><a href="/%E0%B8%9A%E0%B8%97%E0%B8%84%E0%B8%A7%E0%B8%B2%E0%B8%A1" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">บทความ</a></li>
|
||||
<li><a href="/all-products" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">สินค้าทั้งหมด</a></li>
|
||||
<li><a href="/portfolio" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">ผลงาน</a></li>
|
||||
<li><a href="/contact-us" class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">ติดต่อเรา</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div data-animate="fade-up" data-animate-delay="200">
|
||||
<h3 class="font-semibold text-lg mb-4 text-primary-700">สินค้ายอดนิยม</h3>
|
||||
<ul class="space-y-2 text-sm text-neutral-600">
|
||||
{productLinks.map(link => (
|
||||
<li>
|
||||
<a href={link.href} class="hover:text-primary-600 hover:translate-x-1 transition-all inline-block">{link.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: Contact CTA -->
|
||||
<div class="max-w-sm mx-auto" data-animate="fade-up" data-animate-delay="300">
|
||||
<h3 class="font-semibold text-lg mb-4 text-primary-700">ติดต่อเรา</h3>
|
||||
<div class="space-y-3 max-w-xs mx-auto">
|
||||
<a
|
||||
href={`tel:${companyInfo.phone}`}
|
||||
class="flex items-center justify-center gap-2 w-full bg-primary-600 hover:bg-primary-700 text-white py-3 px-4 rounded-xl font-medium transition-all hover:shadow-lg hover:shadow-primary-600/25 hover:-translate-y-0.5 magnetic-btn btn-shimmer"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
โทรเลย
|
||||
</a>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<img
|
||||
src="/images/line-qr.svg"
|
||||
alt="LINE QR Code"
|
||||
class="w-28 h-28 rounded-xl bg-white p-2 shadow-lg hover:shadow-xl transition-shadow"
|
||||
/>
|
||||
<a
|
||||
href="https://line.me/ti/p/~JPPSELECTION"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="flex items-center justify-center gap-2 w-full bg-accent-500 hover:bg-accent-600 text-white py-3 px-4 rounded-xl font-medium transition-all hover:shadow-lg hover:shadow-accent-500/25 hover:-translate-y-0.5 magnetic-btn btn-shimmer"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"/>
|
||||
</svg>
|
||||
แอดไลน์
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Desktop layout (>=1024px): 4 columns, original layout -->
|
||||
<!-- ============================================================ -->
|
||||
<div class="hidden lg:grid lg:grid-cols-4 gap-8 lg:gap-12">
|
||||
<!-- Company Info -->
|
||||
<div class="lg:col-span-1" data-animate="fade-left" data-animate-delay="0">
|
||||
<img src="/images/logo/dealplustech-logo.png" alt="ดีล พลัส เทค" class="h-10 w-auto mb-4 transition-transform hover:scale-105 duration-300" />
|
||||
|
||||
@@ -315,7 +315,7 @@ const articles = (await getCollection('blog')).sort(
|
||||
<p class="text-neutral-600 max-w-2xl mx-auto text-lg">ทุกหมวดเน้นงานอาคาร/โรงงาน/โรงแรม — คลิกเพื่อดูสินค้าเฉพาะหมวด</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 lg:gap-6" data-animate-stagger>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 lg:gap-6" data-animate-stagger>
|
||||
<!-- Category 1: ท่อพีพีอาร์ (ไทย PPR) -->
|
||||
<a href="/all-products?filter=ppr" class="group relative h-64 rounded-3xl overflow-hidden border border-neutral-200 hover:shadow-2xl transition-all duration-500 hover:-translate-y-1">
|
||||
<img src="/images/products-cropped/ppr-pipe_000C.jpg" alt="ท่อพีพีอาร์" class="absolute inset-0 w-full h-full object-cover transition-transform duration-700 group-hover:scale-110" loading="lazy" />
|
||||
|
||||
Reference in New Issue
Block a user