@@ -0,0 +1,2 @@
|
|||||||
|
RESEND_API_KEY=
|
||||||
|
CHATWOOT_TOKEN=
|
||||||
@@ -74,3 +74,30 @@ jobs:
|
|||||||
# cache import timed out (~30s) on every run. The Dockerfile's BuildKit
|
# cache import timed out (~30s) on every run. The Dockerfile's BuildKit
|
||||||
# cache mounts still speed up dependency installs within a build.
|
# cache mounts still speed up dependency installs within a build.
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
# Tell Arcane to pull the freshly-pushed image and recreate the container.
|
||||||
|
#
|
||||||
|
# Create the webhook in Arcane: Settings -> Webhooks -> New, targeting the
|
||||||
|
# compose PROJECT with the "update" action (pulls new images + recreates
|
||||||
|
# changed services). Arcane shows the token (arc_wh_...) exactly once; the
|
||||||
|
# full trigger URL is
|
||||||
|
# https://<arcane-host>/api/webhooks/trigger/arc_wh_xxxxxxxx...
|
||||||
|
# Store that whole URL as the Actions secret ARCANE_WEBHOOK_URL. The
|
||||||
|
# endpoint is unauthenticated apart from the token in the path (rate-limited
|
||||||
|
# to 60 req / 10s per IP), takes no body, and only fires on real pushes to
|
||||||
|
# main — not on PRs, and skipped automatically if the secret is unset.
|
||||||
|
- name: Trigger Arcane deployment
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
env:
|
||||||
|
ARCANE_WEBHOOK_URL: ${{ secrets.ARCANE_WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$ARCANE_WEBHOOK_URL" ]; then
|
||||||
|
echo "ARCANE_WEBHOOK_URL not set; skipping deploy trigger."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$ARCANE_WEBHOOK_URL")
|
||||||
|
echo "Arcane webhook responded with HTTP $code"
|
||||||
|
case "$code" in
|
||||||
|
2*) echo "Deployment triggered." ;;
|
||||||
|
*) echo "::error::Arcane webhook failed (HTTP $code)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ __v0_jsx-dev-runtime.ts
|
|||||||
next.user-config.*
|
next.user-config.*
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# Common ignores
|
# Common ignores
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -84,11 +84,22 @@ Required for full functionality:
|
|||||||
|
|
||||||
```
|
```
|
||||||
RESEND_API_KEY # Resend API key for contact form emails
|
RESEND_API_KEY # Resend API key for contact form emails
|
||||||
NEXT_PUBLIC_CHATWOOT_ACCOUNT_TOKEN # Chatwoot widget token
|
CHATWOOT_TOKEN # Chatwoot website token (served at runtime via GET /api/chatwoot)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Both are read server-side at **runtime**, so they can be supplied via Docker
|
||||||
|
compose `env_file` — no build args or `NEXT_PUBLIC_` prefix needed. The
|
||||||
|
Chatwoot token is intentionally not `NEXT_PUBLIC_`: the client widget fetches
|
||||||
|
it from `/api/chatwoot` (a `force-dynamic` route) instead of having it inlined
|
||||||
|
at build time.
|
||||||
|
|
||||||
## API Routes
|
## API Routes
|
||||||
|
|
||||||
|
### GET /api/chatwoot
|
||||||
|
|
||||||
|
Returns `{ "token": string | null }` — the Chatwoot website token read from
|
||||||
|
the server environment at request time. Consumed by `components/chatwidget.tsx`.
|
||||||
|
|
||||||
### POST /api/send-email
|
### POST /api/send-email
|
||||||
|
|
||||||
Sends contact form submissions to contact@cloudrite.co.nz via Resend.
|
Sends contact form submissions to contact@cloudrite.co.nz via Resend.
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
// Force runtime evaluation so the token is read from the container's
|
||||||
|
// environment on each request instead of being frozen at build time.
|
||||||
|
// This lets Docker compose supply CHATWOOT_TOKEN via env_file.
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
// Chatwoot website tokens are public by design (they ship in the widget),
|
||||||
|
// so exposing it through this endpoint is safe.
|
||||||
|
return NextResponse.json({ token: process.env.CHATWOOT_TOKEN ?? null })
|
||||||
|
}
|
||||||
+59
-15
@@ -1,20 +1,64 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Script from 'next/script'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export function ChatWidget() {
|
export function ChatWidget() {
|
||||||
return (
|
useEffect(() => {
|
||||||
<>
|
let cancelled = false
|
||||||
<Script
|
|
||||||
defer
|
// Fetch the token at runtime from the server so it can come from the
|
||||||
src="http://odoo.cloudrite.co.nz/im_livechat/loader/2"
|
// container environment (compose env_file) instead of being inlined
|
||||||
strategy="afterInteractive"
|
// into the client bundle at build time.
|
||||||
/>
|
fetch('/api/chatwoot')
|
||||||
<Script
|
.then((res) => (res.ok ? res.json() : null))
|
||||||
defer
|
.then((config: { token: string | null } | null) => {
|
||||||
src="http://odoo.cloudrite.co.nz/im_livechat/assets_embed.js"
|
if (cancelled) return
|
||||||
strategy="afterInteractive"
|
|
||||||
/>
|
const chatwootToken = config?.token
|
||||||
</>
|
if (!chatwootToken) {
|
||||||
)
|
console.warn('CHATWOOT_TOKEN not configured')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the site's dark theme. Chatwoot only supports 'light' and
|
||||||
|
// 'auto' (follows prefers-color-scheme), so 'auto' is the darkest
|
||||||
|
// available setting. Must be set before the SDK runs.
|
||||||
|
;(window as any).chatwootSettings = {
|
||||||
|
...(window as any).chatwootSettings,
|
||||||
|
darkMode: 'auto'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Chatwoot widget script
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://app.chatwoot.com/packs/js/sdk.js'
|
||||||
|
script.async = true
|
||||||
|
|
||||||
|
script.onload = () => {
|
||||||
|
// Initialize Chatwoot after script loads
|
||||||
|
if ((window as any).chatwootSDK) {
|
||||||
|
;(window as any).chatwootSDK.run({
|
||||||
|
websiteToken: chatwootToken,
|
||||||
|
baseUrl: 'https://app.chatwoot.com'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
script.onerror = () => {
|
||||||
|
console.error('Failed to load Chatwoot widget')
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(script)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.error('Failed to fetch Chatwoot config')
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
// Cleanup is handled by Chatwoot itself
|
||||||
|
// Don't remove the script as it manages its own lifecycle
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -147,7 +147,7 @@ export function Hero() {
|
|||||||
asChild
|
asChild
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full py-6 text-lg border-border hover:bg-card hover:border-primary/50 transition-all duration-300"
|
className="w-full py-6 text-lg border-border hover:bg-card hover:border-primary/50 hover:text-primary-background transition-all duration-300"
|
||||||
>
|
>
|
||||||
<Link href="#services">Our Services</Link>
|
<Link href="#services">Our Services</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
cloudrite:
|
||||||
|
image: git.cloudrite.co.nz/asowneryt/cloudrite:main
|
||||||
|
container_name: cloudrite
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: false
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- node
|
||||||
|
- -e
|
||||||
|
- "fetch('http://127.0.0.1:3000/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user