Compare commits
16 Commits
f67093f34e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ea12adf484 | |||
| ef8d90651e | |||
| 1dd1ff893e | |||
| d073e021f3 | |||
| c78bd0188b | |||
| cff329a4c4 | |||
| f40e8c638c | |||
| 575fa686ff | |||
| 5ecfc5bb45 | |||
| 8c46bde9ce | |||
| aa163c6c6e | |||
| 68425fe793 | |||
| 21387170f1 | |||
| 92e3de7340 | |||
| bb008f7f8f | |||
| cfebb4cee5 |
135
.dockerignore
Normal file
135
.dockerignore
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
############################################################
|
||||||
|
# Production-ready .dockerignore for a Next.js (Vercel-style) app
|
||||||
|
# Keeps Docker builds fast, lean, and free of development files.
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
# Dependencies (installed inside Docker, never copied)
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Next.js build outputs (always generated during `next build`)
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.vercel/
|
||||||
|
|
||||||
|
# Tests and testing output (not needed in production images)
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
__tests__/
|
||||||
|
__mocks__/
|
||||||
|
jest/
|
||||||
|
cypress/
|
||||||
|
cypress/screenshots/
|
||||||
|
cypress/videos/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
.vitest/
|
||||||
|
vitest.config.*
|
||||||
|
jest.config.*
|
||||||
|
cypress.config.*
|
||||||
|
playwright.config.*
|
||||||
|
*.test.*
|
||||||
|
*.spec.*
|
||||||
|
|
||||||
|
# Local development and editor files
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment variables (only commit template files)
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Docker configuration files (not needed inside build context)
|
||||||
|
Dockerfile*
|
||||||
|
.dockerignore
|
||||||
|
compose.yaml
|
||||||
|
compose.yml
|
||||||
|
docker-compose*.yaml
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# CI/CD configuration files
|
||||||
|
.github/
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.gitea/
|
||||||
|
.travis.yml
|
||||||
|
.circleci/
|
||||||
|
Jenkinsfile
|
||||||
|
|
||||||
|
# Cache directories and temporary data
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
.swc/
|
||||||
|
.turbo/
|
||||||
|
.tmp/
|
||||||
|
.temp/
|
||||||
|
|
||||||
|
# TypeScript build metadata
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Sensitive or unnecessary configuration files
|
||||||
|
*.pem
|
||||||
|
.editorconfig
|
||||||
|
.prettierrc*
|
||||||
|
prettier.config.*
|
||||||
|
.eslintrc*
|
||||||
|
eslint.config.*
|
||||||
|
.stylelintrc*
|
||||||
|
stylelint.config.*
|
||||||
|
.babelrc*
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# OS-specific junk
|
||||||
|
.DS_Store
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# AI/ML tool metadata and configs
|
||||||
|
.cursor/
|
||||||
|
.cursorrules
|
||||||
|
.copilot/
|
||||||
|
.copilotignore
|
||||||
|
.github/copilot/
|
||||||
|
.gemini/
|
||||||
|
.anthropic/
|
||||||
|
.kiro
|
||||||
|
.claude
|
||||||
|
AGENTS.md
|
||||||
|
.agents/
|
||||||
|
|
||||||
|
# AI-generated temp files
|
||||||
|
*.aider*
|
||||||
|
*.copilot*
|
||||||
|
*.chatgpt*
|
||||||
|
*.claude*
|
||||||
|
*.gemini*
|
||||||
|
*.openai*
|
||||||
|
*.anthropic*
|
||||||
55
.gitea/workflows/deploy.yml
Normal file
55
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
name: Build and Deploy NextJS SculptedArc Website
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build-and-deploy:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: sh
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
# 1. Log into your self-hosted gitea container storage layer
|
||||||
|
- name: Log in to gitea Container Registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: gitea.sculpted-arc.co.uk # Change to your gitea domain
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.TOKEN_GITEA }} # Store a personal access token in your repo secrets for security
|
||||||
|
|
||||||
|
# 2. Build the Dockerfile and upload the compiled app image
|
||||||
|
- name: Build and Push Docker Image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: gitea.sculpted-arc.co.uk/james-admin/sculptedarcwebsite:latest
|
||||||
|
|
||||||
|
# 3. Securely SSH to the host using masked secrets
|
||||||
|
- name: Deploy to Server via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SERVER_HOST }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
username: james
|
||||||
|
key: ${{ secrets.SSH_PRIVATE_KEY }} # Completely invisible to anyone viewing code
|
||||||
|
passphrase: ${{ secrets.SSH_PASSPHRASE }}
|
||||||
|
script: |
|
||||||
|
cd /home/james/docker/sculpted-arc-site
|
||||||
|
|
||||||
|
# Authenticate the host machine to pull from gitea's registry
|
||||||
|
docker login -u ${{ gitea.actor }} -p ${{ secrets.TOKEN_GITEA }} gitea.sculpted-arc.co.uk
|
||||||
|
|
||||||
|
# Pull down the fresh image we just built and restart gracefully
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d --remove-orphans
|
||||||
112
Dockerfile
Normal file
112
Dockerfile
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# ============================================
|
||||||
|
# Stage 1: Dependencies Installation Stage
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# IMPORTANT: Node.js Version Maintenance
|
||||||
|
# This Dockerfile uses Node.js 24.13.0-slim, which was the latest LTS version at the time of writing.
|
||||||
|
# To ensure security and compatibility, regularly update the NODE_VERSION ARG to the latest LTS version.
|
||||||
|
ARG NODE_VERSION=24.13.0-slim
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION} AS dependencies
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package-related files first to leverage Docker's caching mechanism
|
||||||
|
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* pnpm-workspace.yaml ./
|
||||||
|
|
||||||
|
# Install project dependencies with frozen lockfile for reproducible builds
|
||||||
|
RUN --mount=type=cache,target=/root/.npm \
|
||||||
|
--mount=type=cache,target=/usr/local/share/.cache/yarn \
|
||||||
|
--mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||||
|
if [ -f package-lock.json ]; then \
|
||||||
|
npm ci --no-audit --no-fund; \
|
||||||
|
elif [ -f yarn.lock ]; then \
|
||||||
|
corepack enable yarn && yarn install --frozen-lockfile --production=false; \
|
||||||
|
elif [ -f pnpm-lock.yaml ]; then \
|
||||||
|
corepack enable pnpm && pnpm install --frozen-lockfile; \
|
||||||
|
else \
|
||||||
|
echo "No lockfile found." && exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Stage 2: Build Next.js application in standalone mode
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION} AS builder
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy project dependencies from dependencies stage
|
||||||
|
COPY --from=dependencies /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Copy application source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Next.js collects completely anonymous telemetry data about general usage.
|
||||||
|
# Learn more here: https://nextjs.org/telemetry
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Build Next.js application
|
||||||
|
# If you want to speed up Docker rebuilds, you can cache the build artifacts
|
||||||
|
# by adding: --mount=type=cache,target=/app/.next/cache
|
||||||
|
# This caches the .next/cache directory across builds, but it also prevents
|
||||||
|
# .next/cache/fetch-cache from being included in the final image, meaning
|
||||||
|
# cached fetch responses from the build won't be available at runtime.
|
||||||
|
RUN if [ -f package-lock.json ]; then \
|
||||||
|
npm run build; \
|
||||||
|
elif [ -f yarn.lock ]; then \
|
||||||
|
corepack enable yarn && yarn build; \
|
||||||
|
elif [ -f pnpm-lock.yaml ]; then \
|
||||||
|
corepack enable pnpm && pnpm build; \
|
||||||
|
else \
|
||||||
|
echo "No lockfile found." && exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Stage 3: Run Next.js application
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION} AS runner
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set production environment variables
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
# Next.js collects completely anonymous telemetry data about general usage.
|
||||||
|
# Learn more here: https://nextjs.org/telemetry
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during the run time.
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Copy production assets
|
||||||
|
COPY --from=builder --chown=node:node /app/public ./public
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown node:node .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=node:node /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
# If you want to persist the fetch cache generated during the build so that
|
||||||
|
# cached responses are available immediately on startup, uncomment this line:
|
||||||
|
# COPY --from=builder --chown=node:node /app/.next/cache ./.next/cache
|
||||||
|
|
||||||
|
# Switch to non-root user for security best practices
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# Expose port 3000 to allow HTTP traffic
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start Next.js standalone server
|
||||||
|
CMD ["node", "server.js"]
|
||||||
37
README.md
37
README.md
@@ -1,36 +1 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
This is the source code for my sculpted arc website. More documentation will be written soon
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
First, run the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
||||||
@@ -4,7 +4,13 @@ import heda_bang from "../../assets/u_mey4kjj5ww-angry-2498.gif"
|
|||||||
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const picWithTextTop = {
|
||||||
|
title: "About Us",
|
||||||
|
text: "Founed in 2025, Sculpted Arc is a web development company dedicated to helping small to medium-sized businesses establish and grow their online presence. We not only specialize in creating custom websites but also offer a wide range of services and support, including e-commerce solutions, business automation, and long-term maintenance.",
|
||||||
|
src: heda_bang.src,
|
||||||
|
reverse: false
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<PicWithText src={heda_bang.src} text="Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?" reverse={false} title="About us"/>
|
<PicWithText {...picWithTextTop}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
32
app/case-studies/[slug]/page.tsx
Normal file
32
app/case-studies/[slug]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// src/app/blog/[slug]/page.tsx
|
||||||
|
import { getPostBySlug } from '@/lib/case-study';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import parse from 'html-react-parser';
|
||||||
|
|
||||||
|
|
||||||
|
interface StudyPageProps {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogPostPage({ params }: StudyPageProps) {
|
||||||
|
// In Next.js 15+, params is a Promise and must be awaited
|
||||||
|
const { slug } = await params;
|
||||||
|
const caseStudy = await getPostBySlug(slug);
|
||||||
|
|
||||||
|
if (!caseStudy) {
|
||||||
|
notFound(); // Triggers the closest 404 page if the file doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="max-w-2xl mx-auto py-10 px-4">
|
||||||
|
<header className="mb-8">
|
||||||
|
<h1 className="text-4xl font-bold mb-2">{caseStudy.title}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Render the parsed HTML safely */}
|
||||||
|
<div className="prose dark:prose-invert">
|
||||||
|
{parse(caseStudy.contentHtml)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import SiteHeader from '../components/site-header';
|
import SiteHeader from '../components/site-header';
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
|
import favicon from "../assets/scws-logo-no-bg.png"
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -15,8 +16,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Sculpted Arc",
|
||||||
description: "Generated by create next app",
|
description: "Web and software solutions for companies and people around the world",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewport = {
|
export const viewport = {
|
||||||
@@ -34,7 +35,11 @@ export default function RootLayout({
|
|||||||
lang="en"
|
lang="en"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col dark:bg-black">
|
<head>
|
||||||
|
<link rel="icon" href={favicon.src} />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body className="min-h-full flex flex-col dark:bg-black dark">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<div className="flex min-h-screen flex-col bg-zinc-50 font-sans dark:bg-black">
|
<div className="flex min-h-screen flex-col bg-zinc-50 font-sans dark:bg-black">
|
||||||
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-between bg-white px-8 py-32 dark:bg-black sm:items-start">
|
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-between bg-white px-8 py-32 dark:bg-black sm:items-start">
|
||||||
|
|||||||
32
app/page.tsx
32
app/page.tsx
@@ -6,48 +6,54 @@ import PicWithText from "@/components/pic-with-text";
|
|||||||
import pictureOne from "../assets/hero-picture-1.png"
|
import pictureOne from "../assets/hero-picture-1.png"
|
||||||
import pictureTwo from "../assets/hero-picture-2.png"
|
import pictureTwo from "../assets/hero-picture-2.png"
|
||||||
import pictureThree from "../assets/hero-picture-3.png"
|
import pictureThree from "../assets/hero-picture-3.png"
|
||||||
|
import pictureFour from "../assets/BrickslipsWebPage.png"
|
||||||
|
import heroPicture from "../assets/mxj-files-planet-27066.gif"
|
||||||
import logo from "../assets/milaoktasafitri-astronomy-9746537_1920.png"
|
import logo from "../assets/milaoktasafitri-astronomy-9746537_1920.png"
|
||||||
import { title } from "process";
|
|
||||||
import { reverse } from "dns";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const contentCards = [
|
const contentCards = [
|
||||||
{
|
{
|
||||||
heading: "Michala Art Shop",
|
heading: "Michala's Art Gallery",
|
||||||
card_text: "This case study was for a small business selling artwork",
|
card_text: "This case study was for a small business selling artwork",
|
||||||
button_text: "click me",
|
button_text: "Read More",
|
||||||
src: pictureOne.src
|
src: pictureOne.src
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Athlito",
|
heading: "Athlito",
|
||||||
card_text: "This was an Ecommerce padel shop",
|
card_text: "This was an Ecommerce padel shop",
|
||||||
button_text: "click me",
|
button_text: "Read More",
|
||||||
src: pictureTwo.src
|
src: pictureTwo.src
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Birchover Reclamation",
|
heading: "Birchover Reclamation",
|
||||||
card_text: "This case study was for a stone yard company",
|
card_text: "This case study was for a stone yard company",
|
||||||
button_text: "click me",
|
button_text: "Read More",
|
||||||
src: pictureThree.src
|
src: pictureThree.src
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Michala Art Shop",
|
heading: "Brickslips Direct LTD",
|
||||||
card_text: "This case study was for a small business selling artwork",
|
card_text: "This case study was for a small business selling brickslips",
|
||||||
button_text: "click me",
|
button_text: "Read More",
|
||||||
src: pictureOne.src
|
src: pictureFour.src
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const picWithText = {
|
const picWithText = {
|
||||||
title: "Building a better web, one pixel at a time",
|
title: "Hand crafted software, built for scale",
|
||||||
text: "At Sculpted Arc, we are passionate about crafting exceptional web solutions that empower businesses to thrive in the digital landscape. With a team of skilled developers and designers, we specialize in creating stunning websites that not only captivate audiences but also drive results. Whether you're a startup looking to establish your online presence or an established company seeking to revamp your website, we have the expertise to bring your vision to life. Our commitment to quality, innovation, and customer satisfaction sets us apart as a trusted partner in your web development journey.",
|
text: "Over the last year, we've had the privilege of working with a diverse range of clients, from local artisans to growing enterprises. Each project has been an opportunity to learn, innovate, and create something truly unique. Our commitment to quality and customer satisfaction has earned us a reputation for excellence in the web development industry.",
|
||||||
src: logo.src,
|
src: logo.src,
|
||||||
reverse: false
|
reverse: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HeroSectionProps = {
|
||||||
|
description: "Sculpted Arc Web Solutions is a web development company. Our passion ranges from creating stunning websites to developing innovative digital solutions. We would love to hear more about your project",
|
||||||
|
buttonText: "Tell us about your project",
|
||||||
|
imageSrc: heroPicture.src
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeroSection />
|
<HeroSection {...HeroSectionProps} />
|
||||||
<PicWithText {...picWithText} />
|
<PicWithText {...picWithText} />
|
||||||
|
|
||||||
<div className="w-full m-auto">
|
<div className="w-full m-auto">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function ServicesPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="mb-12 max-w-2xl text-lg text-body lg:text-xl">
|
<p className="mb-12 max-w-2xl text-lg text-body lg:text-xl">
|
||||||
Sculpted Arc helps companies build, launch, and grow online—from first design
|
Sculpted Arc helps companies build, launch, and grow online—from first design
|
||||||
through long-term support.
|
through long-term support. We offer a wide range of services to meet your needs, including custom web development, e-commerce solutions, content management systems, and more...
|
||||||
</p>
|
</p>
|
||||||
<ServicesFeature />
|
<ServicesFeature />
|
||||||
</>
|
</>
|
||||||
|
|||||||
BIN
assets/BrickslipsWebPage.png
Normal file
BIN
assets/BrickslipsWebPage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 688 KiB |
@@ -2,22 +2,21 @@ import ContentCard from "./ui/content-cards";
|
|||||||
import './hero-section.css'
|
import './hero-section.css'
|
||||||
import heroPictureOne from "../assets/mxj-files-planet-27066.gif"
|
import heroPictureOne from "../assets/mxj-files-planet-27066.gif"
|
||||||
|
|
||||||
export default function HeroSection() {
|
export default function HeroSection({description, buttonText, imageSrc }: {description: string; buttonText: string; imageSrc: string } ) {
|
||||||
return(
|
return(
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap-reverse justify-items-center ">
|
<div className="flex flex-wrap-reverse justify-items-center ">
|
||||||
<div className="p-4 w-full md:w-1/2 shrink-0">
|
<div className="p-4 w-full md:w-1/2 shrink-0">
|
||||||
<img className="w-full h-auto rounded-2xl object-cover" src={heroPictureOne.src} alt="" />
|
<img className="w-full h-auto rounded-2xl object-cover" src={imageSrc} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-1/2">
|
<div className="w-full md:w-1/2">
|
||||||
<h1 className="mb-4 font-bold text-heading text-6xl sm:text-7xl"><span className="text-transparent bg-clip-text bg-gradient-to-r bg-violet-800 from-sky-400">Sculpted Arc</span><br/> Web Solutions</h1>
|
<h1 className="mb-4 font-bold text-heading text-6xl sm:text-7xl"><span className="text-transparent bg-clip-text bg-gradient-to-r bg-violet-800 from-sky-400">Sculpted Arc</span><br/> Web Solutions</h1>
|
||||||
|
|
||||||
<hr className="w-1/4 mb-7 mt-8 border-gray-700 border-2 rounded-2xl"/>
|
<hr className="w-1/4 mb-7 mt-8 border-gray-700 border-2 rounded-2xl"/>
|
||||||
|
|
||||||
<p className="text-lg font-normal text-body lg:text-xl">Here at Sculpted Arc we are invested in creating great website and solving complex web problems Here at Sculpted Arc we are invested in creating great website and solving complex web problems</p>
|
<p className="text-lg font-normal text-body lg:text-xl">{description}</p>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="#" className="transition duration-150 flex w-55 justify-start rounded-3xl text-body bg-neutral-secondary-medium box-border border border-default-medium hover:bg-blue-200 hover:text-black hover:text-heading focus:ring-4 focus:ring-neutral-tertiary shadow-xs font-medium leading-5 rounded-base text-sm px-4 py-2.5 focus:outline-none mt-auto">
|
<a href="#" className="transition duration-150 flex w-55 justify-start rounded-3xl text-body bg-neutral-secondary-medium box-border border border-default-medium hover:bg-blue-200 hover:text-black hover:text-heading focus:ring-4 focus:ring-neutral-tertiary shadow-xs font-medium leading-5 rounded-base text-sm px-4 py-2.5 focus:outline-none mt-auto">
|
||||||
Tell us about your project
|
{buttonText}
|
||||||
<svg className="w-3 h-3 ms-1.5 rtl:rotate-180 -me-0.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 12H5m14 0-4 4m4-4-4-4"/></svg>
|
<svg className="w-3 h-3 ms-1.5 rtl:rotate-180 -me-0.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 12H5m14 0-4 4m4-4-4-4"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,14 +24,7 @@ export default function PicWithText(content: PicWithTextProps){
|
|||||||
<h1 className="mb-4 text-5xl font-bold">{content.title}</h1>
|
<h1 className="mb-4 text-5xl font-bold">{content.title}</h1>
|
||||||
<p className="text-lg font-normal text-body lg:text-xl">{content.text}</p>
|
<p className="text-lg font-normal text-body lg:text-xl">{content.text}</p>
|
||||||
|
|
||||||
<div className="flex items-center w-full my-8">
|
|
||||||
<div className="flex-1 h-px bg-neutral-300 dark:bg-neutral-700" />
|
|
||||||
<div className="px-3">
|
|
||||||
<svg className="w-6 h-6 text-neutral-400 dark:text-neutral-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 11V8a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1Zm0 0v2a4 4 0 0 1-4 4H5m14-6V8a1 1 0 0 0-1-1h-3a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1Zm0 0v2a4 4 0 0 1-4 4h-1"/></svg>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 h-px bg-neutral-300 dark:bg-neutral-700" />
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-300 italic">This is more text, however, this is actually a quote, yes, a quote indeed!</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,63 +1,57 @@
|
|||||||
import './services-feature.css';
|
import './services-feature.css';
|
||||||
|
import { ShoppingCart, Workflow, Server, CodeXml, Plug, Globe } from 'lucide-react';
|
||||||
|
|
||||||
export type Service = {
|
export type Service = {
|
||||||
title: string;
|
title: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const services: Service[] = [
|
const services: Service[] = [
|
||||||
{
|
{
|
||||||
title: 'Custom Web Design',
|
title: 'Company Automation',
|
||||||
|
icon: <Workflow />,
|
||||||
description:
|
description:
|
||||||
'Tailored layouts and branding that reflect your business and engage your audience from the first visit.',
|
'Streamline operations and boost efficiency with custom automation solutions tailored to your business needs.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Web Development',
|
title: 'Web Development',
|
||||||
|
icon: <CodeXml />,
|
||||||
description:
|
description:
|
||||||
'Fast, reliable sites built with modern frameworks—scalable architecture that grows with your company.',
|
'Fast, reliable sites built with modern frameworks—scalable architecture that grows with your company.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'E-commerce',
|
title: 'E-commerce',
|
||||||
|
icon: <ShoppingCart />,
|
||||||
description:
|
description:
|
||||||
'Online stores with smooth checkout, inventory management, and payment integrations that convert.',
|
'Online stores with smooth checkout, inventory management, and payment integrations that convert. Options inclide Shopify, WooCommerce, and custom solutions.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Performance & SEO',
|
title: 'Custom Plugins & Extensions',
|
||||||
|
icon: <Plug />,
|
||||||
description:
|
description:
|
||||||
'Optimised load times, accessibility, and search visibility so customers can find and use your site.',
|
'Extend your website’s functionality with custom plugins and extensions tailored to your specific needs.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Hosting & Maintenance',
|
title: 'Hosting & Maintenance',
|
||||||
|
icon: <Server />,
|
||||||
description:
|
description:
|
||||||
'Secure hosting, updates, backups, and monitoring so your site stays online and up to date.',
|
'Secure hosting, updates, emails, backups, and monitoring so your site stays online and up to date.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'APIs & Integrations',
|
title: 'APIs & Integrations',
|
||||||
|
icon: <Globe />,
|
||||||
description:
|
description:
|
||||||
'Connect CRMs, booking systems, analytics, and third-party tools into one cohesive workflow.',
|
'Connect CRMs, booking systems, analytics, and third-party tools into one cohesive workflow.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function ServiceFeatureCard({ title, description }: Service) {
|
function ServiceFeatureCard({ title, icon, description }: Service) {
|
||||||
return (
|
return (
|
||||||
<article className="service-feature">
|
<article className="service-feature">
|
||||||
<div className="service-feature-body">
|
<div className="service-feature-body">
|
||||||
<div className="service-feature-icon" aria-hidden="true">
|
<div className="service-feature-icon" aria-hidden="true">
|
||||||
<svg
|
{icon}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="service-feature-title">{title}</h3>
|
<h3 className="service-feature-title">{title}</h3>
|
||||||
<p className="service-feature-description">{description}</p>
|
<p className="service-feature-description">{description}</p>
|
||||||
@@ -74,7 +68,7 @@ type ServicesFeatureProps = {
|
|||||||
|
|
||||||
export default function ServicesFeature({
|
export default function ServicesFeature({
|
||||||
heading = 'What we offer',
|
heading = 'What we offer',
|
||||||
subheading = 'End-to-end web solutions for businesses of every size.',
|
subheading = 'End-to-end web solutions for small to medium-sized businesses ',
|
||||||
items = services,
|
items = services,
|
||||||
}: ServicesFeatureProps) {
|
}: ServicesFeatureProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import './typing-words.css';
|
import './typing-words.css';
|
||||||
|
|
||||||
const WORDS = ['plan', 'idea', 'problem', 'goals'] as const;
|
const WORDS = ['plan', 'vision', 'problem', 'goals', 'business'] as const;
|
||||||
|
|
||||||
const TYPING_MS = 90;
|
const TYPING_MS = 90;
|
||||||
const DELETING_MS = 55;
|
const DELETING_MS = 55;
|
||||||
|
|||||||
32
compose.yml
Normal file
32
compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
services:
|
||||||
|
# Node.js service (use with: docker compose up nextjs-standalone --build)
|
||||||
|
nextjs-standalone:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: nextjs-standalone-image
|
||||||
|
container_name: nextjs-standalone-frontend
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: "3000"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Bun service (use with: docker compose up nextjs-standalone-with-bun --build)
|
||||||
|
nextjs-standalone-with-bun:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.bun
|
||||||
|
image: nextjs-standalone-bun-image
|
||||||
|
container_name: nextjs-standalone-bun-container
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: "3000"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik_proxy:
|
||||||
|
external: true
|
||||||
5
content/case-studies/study-one.md
Normal file
5
content/case-studies/study-one.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: "Case Study One"
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the fist case study
|
||||||
1
content/case-studies/study-two.md
Normal file
1
content/case-studies/study-two.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Hello, this is case study two
|
||||||
19
deploy.yml.bak
Normal file
19
deploy.yml.bak
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Build and Deploy NextJS SculptedArc Website
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: sh
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 1. Log into your self-hosted gitea container storage layer
|
||||||
|
- name: Log in to gitea Container Registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: gitea.sculpted-arc.co.uk # Change to your gitea domain
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.TOKEN_GITEA }} # Store a personal access token in your repo secrets for security
|
||||||
33
lib/case-study.ts
Normal file
33
lib/case-study.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// src/lib/blog.ts
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
import { remark } from 'remark';
|
||||||
|
import html from 'remark-html';
|
||||||
|
|
||||||
|
const postsDirectory = path.join(process.cwd(), 'content/case-studies');
|
||||||
|
|
||||||
|
export async function getPostBySlug(slug: string) {
|
||||||
|
const fullPath = path.join(postsDirectory, `${slug}.md`);
|
||||||
|
|
||||||
|
// Guard against file system traversal attacks or missing files
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
|
||||||
|
// Use gray-matter to parse the study metadata section
|
||||||
|
const { data, content } = matter(fileContents);
|
||||||
|
|
||||||
|
// Use remark to convert markdown into HTML string
|
||||||
|
const processedContent = await remark().use(html).process(content);
|
||||||
|
const contentHtml = processedContent.toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
contentHtml,
|
||||||
|
title: data.title || 'Untitled Study',
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: "standalone",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -9,9 +9,14 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"html-react-parser": "^6.1.3",
|
||||||
|
"lucide-react": "^1.17.0",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.4",
|
||||||
|
"remark": "^15.0.1",
|
||||||
|
"remark-html": "^16.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
745
pnpm-lock.yaml
generated
745
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
pnpm-workspace.yaml
Normal file
7
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
allowBuilds:
|
||||||
|
sharp@0.34.5: true
|
||||||
|
unrs-resolver@1.11.1: true
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- sharp
|
||||||
|
- unrs-resolver
|
||||||
Reference in New Issue
Block a user