@@ -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 mounts still speed up dependency installs within a build.
|
||||
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.*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env*.local
|
||||
!.env.example
|
||||
|
||||
# Common ignores
|
||||
node_modules/
|
||||
|
||||
@@ -84,11 +84,22 @@ Required for full functionality:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
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'
|
||||
|
||||
import Script from 'next/script'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function ChatWidget() {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
defer
|
||||
src="http://odoo.cloudrite.co.nz/im_livechat/loader/2"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script
|
||||
defer
|
||||
src="http://odoo.cloudrite.co.nz/im_livechat/assets_embed.js"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
// Fetch the token at runtime from the server so it can come from the
|
||||
// container environment (compose env_file) instead of being inlined
|
||||
// into the client bundle at build time.
|
||||
fetch('/api/chatwoot')
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then((config: { token: string | null } | null) => {
|
||||
if (cancelled) return
|
||||
|
||||
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
|
||||
variant="outline"
|
||||
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>
|
||||
</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/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// 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