Try building
This commit is contained in:
parent
112d9dd063
commit
5bfbb3b88c
15 changed files with 597 additions and 104 deletions
6
.env.example
Normal file
6
.env.example
Normal file
|
@ -0,0 +1,6 @@
|
|||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
FROM_EMAIL=
|
||||
TO_EMAIL=
|
44
Dockerfile
Normal file
44
Dockerfile
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built assets and necessary files
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
|
||||
# Install only production dependencies
|
||||
RUN pnpm install --prod --frozen-lockfile
|
||||
|
||||
# Copy environment variables if needed
|
||||
COPY .env.production ./.env
|
||||
|
||||
# Expose the port your app runs on
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "./dist/server/entry.mjs"]
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
services:
|
||||
app:
|
||||
image: ${IMAGE}
|
||||
ports:
|
||||
- "${APP_PORT}:3000"
|
||||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3000
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- FROM_EMAIL=${FROM_EMAIL}
|
||||
- TO_EMAIL=${TO_EMAIL}
|
||||
restart: unless-stopped
|
|
@ -12,6 +12,7 @@
|
|||
"@astrojs/solid-js": "^5.0.4",
|
||||
"@astrojs/tailwind": "^5.1.5",
|
||||
"astro": "^5.1.8",
|
||||
"nodemailer": "^6.9.16",
|
||||
"solid-js": "^1.9.4",
|
||||
"tailwindcss": "^3.4.17"
|
||||
},
|
||||
|
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -17,6 +17,9 @@ importers:
|
|||
astro:
|
||||
specifier: ^5.1.8
|
||||
version: 5.1.8(jiti@1.21.7)(rollup@4.31.0)(typescript@5.7.3)(yaml@2.7.0)
|
||||
nodemailer:
|
||||
specifier: ^6.9.16
|
||||
version: 6.9.16
|
||||
solid-js:
|
||||
specifier: ^1.9.4
|
||||
version: 1.9.4
|
||||
|
@ -1351,6 +1354,10 @@ packages:
|
|||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
nodemailer@6.9.16:
|
||||
resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -3552,6 +3559,8 @@ snapshots:
|
|||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
nodemailer@6.9.16: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
|
140
src/components/ContactForm.tsx
Normal file
140
src/components/ContactForm.tsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
import { createSignal, type Component } from "solid-js";
|
||||
import { Show } from "solid-js/web";
|
||||
|
||||
const ContactForm: Component = () => {
|
||||
const [email, setEmail] = createSignal("");
|
||||
const [message, setMessage] = createSignal("");
|
||||
const [status, setStatus] = createSignal<
|
||||
"idle" | "sending" | "success" | "error"
|
||||
>("idle");
|
||||
const [errorMessage, setErrorMessage] = createSignal("");
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
setStatus("sending");
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email(),
|
||||
message: message(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to send message");
|
||||
}
|
||||
|
||||
setStatus("success");
|
||||
setEmail("");
|
||||
setMessage("");
|
||||
|
||||
// Reset success status after 3 seconds
|
||||
setTimeout(() => setStatus("idle"), 3000);
|
||||
} catch (error) {
|
||||
setStatus("error");
|
||||
setErrorMessage(
|
||||
error instanceof Error ? error.message : "Failed to send message",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form class="space-y-6" onSubmit={handleSubmit}>
|
||||
<div class="form-control w-full">
|
||||
<label for="email" class="label">
|
||||
<span class="label-text text-neutral-content">Email</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
class="input input-bordered w-full"
|
||||
placeholder="your@email.com"
|
||||
value={email()}
|
||||
onInput={(e) => setEmail(e.currentTarget.value)}
|
||||
disabled={status() === "sending"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control w-full">
|
||||
<label for="message" class="label">
|
||||
<span class="label-text text-neutral-content">Message</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
required
|
||||
class="textarea textarea-bordered h-32"
|
||||
placeholder="How can we help you?"
|
||||
value={message()}
|
||||
onInput={(e) => setMessage(e.currentTarget.value)}
|
||||
disabled={status() === "sending"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when={status() === "error"}>
|
||||
<div class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{errorMessage() || "Error sending message. Please try again."}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={status() === "success"}>
|
||||
<div class="alert alert-success">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Message sent successfully!</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
disabled={status() === "sending"}
|
||||
>
|
||||
{status() === "sending" ? (
|
||||
<>
|
||||
<span class="loading loading-spinner"></span>
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
"Send Message"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactForm;
|
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
import { siteConfig } from "../config/site";
|
||||
---
|
||||
|
||||
<footer
|
||||
class="footer footer-center p-4 bg-base-300 text-base-content"
|
||||
role="contentinfo"
|
||||
|
@ -5,7 +9,9 @@
|
|||
<aside>
|
||||
<p>
|
||||
<span class="sr-only">Copyright</span>
|
||||
© 2024 - All rights reserved by Your Business Name
|
||||
© {new Date().getFullYear()} - All rights reserved by {
|
||||
siteConfig.name
|
||||
}
|
||||
</p>
|
||||
</aside>
|
||||
</footer>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
import { siteConfig } from "../config/site";
|
||||
---
|
||||
|
||||
<header class="navbar bg-base-100 shadow-lg" role="banner">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
|
@ -27,29 +31,45 @@
|
|||
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
|
||||
role="menu"
|
||||
>
|
||||
<li role="none"><a href="/" role="menuitem">Home</a></li>
|
||||
<li role="none">
|
||||
<a href="/services" role="menuitem">Services</a>
|
||||
</li>
|
||||
<li role="none">
|
||||
<a href="/contact" role="menuitem">Contact</a>
|
||||
</li>
|
||||
{
|
||||
siteConfig.header.nav.map(({ text, href }) => (
|
||||
<li role="none">
|
||||
<a href={href} role="menuitem">
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/" class="btn btn-ghost text-xl" aria-label="Home"
|
||||
>Your Business</a
|
||||
<a
|
||||
href={siteConfig.header.logo.href}
|
||||
class="btn btn-ghost text-xl"
|
||||
aria-label="Home"
|
||||
>
|
||||
{siteConfig.header.logo.text}
|
||||
</a>
|
||||
</div>
|
||||
<nav class="navbar-center hidden lg:flex" aria-label="Main navigation">
|
||||
<ul class="menu menu-horizontal px-1" role="menubar">
|
||||
<li role="none"><a href="/" role="menuitem">Home</a></li>
|
||||
<li role="none">
|
||||
<a href="/services" role="menuitem">Services</a>
|
||||
</li>
|
||||
<li role="none"><a href="/contact" role="menuitem">Contact</a></li>
|
||||
{
|
||||
siteConfig.header.nav.map(({ text, href }) => (
|
||||
<li role="none">
|
||||
<a href={href} role="menuitem">
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="navbar-end">
|
||||
<a href="/contact" class="btn btn-primary" role="button">Get Started</a>
|
||||
<a
|
||||
href={siteConfig.header.cta.href}
|
||||
class="btn btn-primary"
|
||||
role="button"
|
||||
>
|
||||
{siteConfig.header.cta.text}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
|
106
src/config/site.ts
Normal file
106
src/config/site.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
type Card = {
|
||||
title: string;
|
||||
content: string;
|
||||
variant: "primary" | "secondary" | "accent" | "neutral";
|
||||
};
|
||||
|
||||
type StatChange = {
|
||||
value: string;
|
||||
percentage: string;
|
||||
direction: "up" | "down";
|
||||
};
|
||||
|
||||
type Stat = {
|
||||
title: string;
|
||||
value: string;
|
||||
change?: StatChange;
|
||||
};
|
||||
|
||||
export const siteConfig = {
|
||||
name: "Atash Consulting",
|
||||
description: "Software Consulting based in Edmonton, Alberta",
|
||||
|
||||
header: {
|
||||
logo: {
|
||||
text: "Atash Consulting",
|
||||
href: "/",
|
||||
},
|
||||
nav: [
|
||||
{
|
||||
text: "Home",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
text: "Services",
|
||||
href: "/services",
|
||||
},
|
||||
{
|
||||
text: "Contact",
|
||||
href: "/contact",
|
||||
},
|
||||
],
|
||||
cta: {
|
||||
text: "Get Started",
|
||||
href: "/contact",
|
||||
},
|
||||
},
|
||||
|
||||
hero: {
|
||||
title: "Atash Consulting",
|
||||
description: "Software Consulting based in Edmonton, Alberta",
|
||||
},
|
||||
|
||||
featureCards: {
|
||||
enabled: true,
|
||||
cards: [
|
||||
{
|
||||
title: "Web Development",
|
||||
content: "Functional, accessible, and beautiful websites.",
|
||||
variant: "primary",
|
||||
},
|
||||
{
|
||||
title: "Mobile App Development",
|
||||
content: "iOS, Android, and cross-platform mobile applications.",
|
||||
variant: "secondary",
|
||||
},
|
||||
{
|
||||
title: "DevOps",
|
||||
content: "Anything from CI/CD to end-to-end UI testing.",
|
||||
variant: "secondary",
|
||||
},
|
||||
{
|
||||
title: "IT Support Processes",
|
||||
content:
|
||||
"Providing expert technical support expertise, backed by over a decade of client support experience.",
|
||||
variant: "primary",
|
||||
},
|
||||
] as Card[],
|
||||
},
|
||||
|
||||
statistics: {
|
||||
enabled: true,
|
||||
title: "Company Statistics",
|
||||
stats: [
|
||||
{
|
||||
title: "Years of Experience",
|
||||
value: "10+",
|
||||
},
|
||||
{
|
||||
title: "Reviews",
|
||||
value: "5⭐️",
|
||||
},
|
||||
] as Stat[],
|
||||
},
|
||||
|
||||
contact: {
|
||||
title: "Contact Us",
|
||||
description: "Ready to get started? Reach out to us for a consultation.",
|
||||
cta: {
|
||||
text: "Get in Touch",
|
||||
href: "/contact",
|
||||
ariaLabel: "Contact us for consultation",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
|
@ -1,27 +1,29 @@
|
|||
---
|
||||
import Header from "../components/Header.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { siteConfig } from "../config/site";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = "Atash Consulting",
|
||||
description = "Technical Excellence with a Human Touch",
|
||||
} = Astro.props;
|
||||
const { title = siteConfig.name, description = siteConfig.description } =
|
||||
Astro.props;
|
||||
|
||||
const metaTitle =
|
||||
title === siteConfig.name ? title : `${title} | ${siteConfig.name}`;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="coffee">
|
||||
<html lang="en" data-theme="sunset">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<meta name="description" content={description} />
|
||||
<title>{title}</title>
|
||||
<title>{metaTitle}</title>
|
||||
<style>
|
||||
.skip-to-content {
|
||||
position: absolute;
|
||||
|
|
58
src/pages/api/contact.ts
Normal file
58
src/pages/api/contact.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import type { APIRoute } from "astro";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: import.meta.env.SMTP_HOST,
|
||||
port: parseInt(import.meta.env.SMTP_PORT),
|
||||
secure: import.meta.env.SMTP_PORT === "465",
|
||||
auth: {
|
||||
user: import.meta.env.SMTP_USER,
|
||||
pass: import.meta.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const data = await request.json();
|
||||
const { email, message } = data;
|
||||
|
||||
if (!email || !message) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: "Email and message are required",
|
||||
}),
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
await transporter.sendMail({
|
||||
from: import.meta.env.FROM_EMAIL,
|
||||
to: [import.meta.env.TO_EMAIL],
|
||||
subject: `New Contact Form Submission from ${email}`,
|
||||
text: message,
|
||||
html: `
|
||||
<h2>New Contact Form Submission</h2>
|
||||
<p><strong>From:</strong> ${email}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${message.replace(/\n/g, "<br>")}</p>
|
||||
`,
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: "Email sent successfully",
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: "Error sending email",
|
||||
}),
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
25
src/pages/contact.astro
Normal file
25
src/pages/contact.astro
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import ContactForm from "../components/ContactForm";
|
||||
import { siteConfig } from "../config/site";
|
||||
|
||||
const pageMetaInfo = {
|
||||
title: "Contact | " + siteConfig.name,
|
||||
description: "Get in touch with us for your software development needs",
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={pageMetaInfo.title} description={pageMetaInfo.description}>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<section class="card bg-neutral text-neutral-content shadow-xl">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title text-3xl">Contact Us</h1>
|
||||
<p class="text-lg">
|
||||
Ready to get started? Send us a message and we'll get back
|
||||
to you shortly.
|
||||
</p>
|
||||
<ContactForm client:load />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
|
@ -1,111 +1,104 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import { siteConfig } from "../config/site";
|
||||
|
||||
const pageMetaInfo = {
|
||||
title: "Atash Consulting",
|
||||
description:
|
||||
"Welcome to Atash Consulting - Technical Excellence with a Human Touch",
|
||||
};
|
||||
|
||||
const layoutProps = {
|
||||
...(pageMetaInfo.title?.trim() && { title: pageMetaInfo.title }),
|
||||
...(pageMetaInfo.description?.trim() && {
|
||||
description: pageMetaInfo.description,
|
||||
}),
|
||||
title: siteConfig.name,
|
||||
description: `Welcome to ${siteConfig.name} - ${siteConfig.description}`,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout {...layoutProps}>
|
||||
<Layout title={pageMetaInfo.title} description={pageMetaInfo.description}>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<section
|
||||
class="card bg-base-100 shadow-xl"
|
||||
class="card bg-neutral text-neutral-content shadow-xl"
|
||||
aria-labelledby="welcome-heading"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h1 id="welcome-heading" class="card-title text-3xl">
|
||||
{pageMetaInfo.title || "Atash Consulting"}
|
||||
{siteConfig.hero.title}
|
||||
</h1>
|
||||
<p class="text-base-content/70">
|
||||
{
|
||||
pageMetaInfo.description ||
|
||||
"Technical Excellence with a Human Touch"
|
||||
}
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
|
||||
<article class="card bg-primary text-primary-content">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Our Mission</h2>
|
||||
<p>
|
||||
To deliver exceptional value through innovative
|
||||
solutions.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card bg-secondary text-secondary-content">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Our Vision</h2>
|
||||
<p>
|
||||
Leading the industry with cutting-edge
|
||||
technology.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p>{siteConfig.hero.description}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="stats shadow mt-8 w-full"
|
||||
aria-label="Company Statistics"
|
||||
>
|
||||
<div class="stat">
|
||||
<div class="stat-title" id="clients-stat">Total Clients</div>
|
||||
<div class="stat-value" aria-labelledby="clients-stat">
|
||||
500+
|
||||
</div>
|
||||
<div class="stat-desc" aria-label="Increase of 40 clients (2%)">
|
||||
↗︎ 40 (2%)
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
|
||||
{
|
||||
siteConfig.featureCards.cards.map((card) => (
|
||||
<article
|
||||
class={
|
||||
card.variant === "primary"
|
||||
? "card bg-primary text-primary-content"
|
||||
: "card bg-secondary text-secondary-content"
|
||||
}
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{card.title}</h2>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title" id="projects-stat">
|
||||
Projects Completed
|
||||
</div>
|
||||
<div class="stat-value" aria-labelledby="projects-stat">
|
||||
1,200
|
||||
</div>
|
||||
<div
|
||||
class="stat-desc"
|
||||
aria-label="Increase of 90 projects (14%)"
|
||||
>
|
||||
↗︎ 90 (14%)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title" id="success-stat">Success Rate</div>
|
||||
<div class="stat-value" aria-labelledby="success-stat">98%</div>
|
||||
<div class="stat-desc" aria-label="Increase of 2%">↗︎ 2%</div>
|
||||
</div>
|
||||
</section>
|
||||
{
|
||||
siteConfig.statistics.enabled &&
|
||||
siteConfig.statistics.stats.length > 0 && (
|
||||
<section
|
||||
class="stats shadow mt-8 w-full bg-neutral text-neutral-content"
|
||||
aria-label={siteConfig.statistics.title}
|
||||
>
|
||||
{siteConfig.statistics.stats.map((stat) => (
|
||||
<div class="stat">
|
||||
<div
|
||||
class="stat-title"
|
||||
id={`${stat.title.toLowerCase()}-stat`}
|
||||
>
|
||||
{stat.title}
|
||||
</div>
|
||||
<div
|
||||
class="stat-value"
|
||||
aria-labelledby={`${stat.title.toLowerCase()}-stat`}
|
||||
>
|
||||
{stat.value}
|
||||
</div>
|
||||
{stat.change && (
|
||||
<div
|
||||
class="stat-desc"
|
||||
aria-label={`${
|
||||
stat.change.direction === "up"
|
||||
? "Increase"
|
||||
: "Decrease"
|
||||
} of ${stat.change.value} (${stat.change.percentage}%)`}
|
||||
>
|
||||
{stat.change.direction === "up"
|
||||
? "↗︎"
|
||||
: "↘︎"}{" "}
|
||||
{stat.change.value} (
|
||||
{stat.change.percentage}%)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<section
|
||||
class="card bg-base-100 shadow-xl mt-8"
|
||||
class="card bg-neutral text-neutral-content shadow-xl mt-8 hidden lg:block"
|
||||
aria-labelledby="contact-heading"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 id="contact-heading" class="card-title">Contact Us</h2>
|
||||
<p>Ready to get started? Reach out to us for a consultation.</p>
|
||||
<h2 id="contact-heading" class="card-title">
|
||||
{siteConfig.contact.title}
|
||||
</h2>
|
||||
<p>{siteConfig.contact.description}</p>
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="window.location.href='/contact'"
|
||||
aria-label="Contact us for consultation"
|
||||
onclick={`window.location.href='${siteConfig.contact.cta.href}'`}
|
||||
aria-label={siteConfig.contact.cta.ariaLabel}
|
||||
>
|
||||
Get in Touch
|
||||
{siteConfig.contact.cta.text}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
68
src/pages/services.astro
Normal file
68
src/pages/services.astro
Normal file
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import { siteConfig } from "../config/site";
|
||||
|
||||
const pageMetaInfo = {
|
||||
title: "Services | " + siteConfig.name,
|
||||
description:
|
||||
"Comprehensive software development and IT services in Edmonton, Alberta",
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={pageMetaInfo.title} description={pageMetaInfo.description}>
|
||||
<div class="max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<!-- Increased from 4xl to 5xl -->
|
||||
<section class="card bg-neutral text-neutral-content shadow-xl">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title text-3xl">Our Services</h1>
|
||||
<p class="text-lg">
|
||||
Comprehensive software development and IT solutions tailored
|
||||
to your needs.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{
|
||||
siteConfig.featureCards.cards.map((service, index) => (
|
||||
<section class="card bg-neutral text-neutral-content shadow-xl basis-[calc(50%-0.5rem)]">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class={`badge badge-${service.variant} badge-lg`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<h2 class="card-title text-2xl">
|
||||
{service.title}
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-lg mt-4">{service.content}</p>
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="card bg-neutral text-neutral-content shadow-xl mt-8 hidden lg:block"
|
||||
aria-labelledby="contact-heading"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 id="contact-heading" class="card-title">
|
||||
{siteConfig.contact.title}
|
||||
</h2>
|
||||
<p>{siteConfig.contact.description}</p>
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={`window.location.href='${siteConfig.contact.cta.href}'`}
|
||||
aria-label={siteConfig.contact.cta.ariaLabel}
|
||||
>
|
||||
{siteConfig.contact.cta.text}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
|
@ -6,6 +6,6 @@ export default {
|
|||
},
|
||||
plugins: [require("daisyui")],
|
||||
daisyui: {
|
||||
themes: ["coffee", "dark", "business"],
|
||||
themes: ["coffee", "dark", "sunset"],
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue