init project structer

This commit is contained in:
2025-10-08 14:12:48 +07:00
commit 9c177abda0
22 changed files with 6737 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Dependencies
node_modules
# Next.js
apps/web/.next
apps/web/out
# Go
apps/server-go/catsave
apps/server-go/static
# Build
dist
# Env
.env
.env*.local
# OS
.DS_Store
Thumbs.db
# IDE
.vscode
.idea
*.swp

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80
}

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
# 🐱 CatSave - Go + Next.js Monorepo
Single binary application with Go backend + Next.js frontend.
## 📦 Structure
```
catsave/
├── apps/
│ ├── web/ # Next.js frontend (static export)
│ └── server-go/ # Go backend + static file server
├── scripts/ # Build scripts
└── dist/ # Build output
```
## 🚀 Development
### Quick Start
```bash
# 1. Install dependencies
npm install
# 2. Option A: Dev Next.js only
npm run dev:web # http://localhost:3000
# 3. Option B: Dev with Go
npm run build:dev # Build Next.js + sync to Go
npm run dev:go # Run Go server
```
### Workflow
**Frontend Development (Next.js):**
```bash
npm run dev:web # Hot reload on :3000
```
**Full Stack Development (Next.js + Go):**
```bash
# Terminal 1: Build & sync on changes
npm run build:dev
# Terminal 2: Run Go server
npm run dev:go # http://localhost:8080
```
**After changing Next.js code:**
```bash
npm run build:dev # Re-build and sync
# Go server will auto-serve new files
```
## 🔨 Build
### Development Build (Quick)
```bash
npm run build:dev
# → Builds Next.js + syncs to Go static folder
```
### Production Binary
```bash
npm run build
# → Builds Next.js + Go binary
# → Output: dist/catsave (single binary)
```
### Available Commands
| Command | Description |
|---------|-------------|
| `npm run dev:web` | Dev Next.js only (port 3000) |
| `npm run dev:go` | Run Go server (port 8080) |
| `npm run build:web` | Build Next.js static export |
| `npm run sync:static` | Copy Next.js → Go static |
| `npm run build:dev` | Build + sync (development) |
| `npm run build` | Full production build |
## 🎯 Deploy
```bash
# Just copy the binary
./dist/catsave
# Runs on http://localhost:8080
```
## 🏗️ Architecture
- **Go**: API + serve static files (embedded)
- **Next.js**: Static export (HTML/CSS/JS)
- **Result**: One binary file (~10-30MB)
## 📝 Tech Stack
- Go 1.21+
- Next.js 14
- TypeScript
- Tailwind CSS

2
apps/server-go/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# This ensures the directory is tracked by git

5
apps/server-go/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/catsave/server
go 1.21
require github.com/gorilla/mux v1.8.1

2
apps/server-go/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=

80
apps/server-go/main.go Normal file
View File

@@ -0,0 +1,80 @@
package main
import (
"embed"
"encoding/json"
"io/fs"
"log"
"net/http"
"os"
"time"
"github.com/gorilla/mux"
)
//go:embed static/*
var staticFiles embed.FS
type HealthResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Timestamp string `json:"timestamp"`
}
func main() {
r := mux.NewRouter()
// API Routes
api := r.PathPrefix("/api").Subrouter()
api.HandleFunc("/health", healthHandler).Methods("GET")
api.HandleFunc("/cats", catsHandler).Methods("GET")
// Serve static files (Next.js build)
staticFS, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatal(err)
}
// Serve static files
r.PathPrefix("/").Handler(http.FileServer(http.FS(staticFS)))
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("🚀 CatSave server starting on http://localhost:%s\n", port)
log.Printf("📱 Web UI: http://localhost:%s\n", port)
log.Printf("🔌 API: http://localhost:%s/api/health\n", port)
if err := http.ListenAndServe(":"+port, r); err != nil {
log.Fatal(err)
}
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := HealthResponse{
Status: "ok",
Message: "Go backend is running! 🐹",
Timestamp: time.Now().Format(time.RFC3339),
}
json.NewEncoder(w).Encode(response)
}
func catsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
cats := []map[string]interface{}{
{"id": 1, "name": "Whiskers", "breed": "Persian"},
{"id": 2, "name": "Shadow", "breed": "Siamese"},
{"id": 3, "name": "Luna", "breed": "British Shorthair"},
}
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Hello from Go!",
"cats": cats,
})
}

4
apps/web/.eslintrc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "next/core-web-vitals"
}

5
apps/web/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

21
apps/web/next.config.js Normal file
View File

@@ -0,0 +1,21 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
// Static Export - Build to pure HTML/CSS/JS
output: 'export',
// Disable features not supported in static export
images: {
unoptimized: true,
},
// Optional: add trailing slash
trailingSlash: true,
// Optional: if Go serves from /app path
// basePath: '/app',
}
module.exports = nextConfig

27
apps/web/package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "@catsave/web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "next lint"
},
"dependencies": {
"next": "^14.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^20.14.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.5",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.6",
"typescript": "^5.5.4"
}
}

View File

@@ -0,0 +1,10 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;

View File

@@ -0,0 +1,24 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1,20 @@
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'CatSave',
description: 'Go + Next.js Single Binary Application',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

72
apps/web/src/app/page.tsx Normal file
View File

@@ -0,0 +1,72 @@
'use client'
import { useState, useEffect } from 'react'
export default function Home() {
const [apiData, setApiData] = useState<any>(null)
useEffect(() => {
// Call Go API
fetch('/api/health')
.then(res => res.json())
.then(data => setApiData(data))
.catch(err => console.error(err))
}, [])
return (
<main className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50 to-indigo-50 flex items-center justify-center p-6">
<div className="max-w-2xl w-full">
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-3xl shadow-lg mb-6">
<span className="text-5xl">🐱</span>
</div>
<h1 className="text-5xl font-bold text-gray-900 mb-4">
CatSave
</h1>
<p className="text-xl text-gray-600 mb-8">
Go + Next.js Single Binary
</p>
</div>
<div className="bg-white rounded-3xl shadow-2xl p-8 space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-6 border-2 border-blue-100">
<h3 className="text-lg font-bold text-gray-900 mb-2">Frontend</h3>
<p className="text-sm text-gray-600">Next.js 14 + React 18</p>
<p className="text-xs text-blue-600 mt-2"> Static Export</p>
</div>
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl p-6 border-2 border-green-100">
<h3 className="text-lg font-bold text-gray-900 mb-2">Backend</h3>
<p className="text-sm text-gray-600">Go 1.21+</p>
<p className="text-xs text-green-600 mt-2"> Embedded</p>
</div>
</div>
<div className="bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl p-6 border-2 border-purple-100">
<h3 className="text-lg font-bold text-gray-900 mb-3">API Status</h3>
{apiData ? (
<div className="space-y-2 text-sm">
<p> Status: <span className="font-mono text-green-600">{apiData.status}</span></p>
<p> Time: <span className="font-mono text-gray-600">{apiData.timestamp}</span></p>
<p>🐹 Server: <span className="font-mono text-blue-600">{apiData.message}</span></p>
</div>
) : (
<p className="text-sm text-gray-500">Loading...</p>
)}
</div>
<div className="text-center pt-4">
<a
href="/setup"
className="inline-block px-8 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-xl shadow-lg transition-all"
>
Setup Wizard
</a>
</div>
</div>
</div>
</main>
)
}

View File

@@ -0,0 +1,25 @@
'use client'
export default function SetupPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50 to-indigo-50 flex items-center justify-center p-6">
<div className="max-w-2xl w-full">
<div className="bg-white rounded-3xl shadow-2xl p-8">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
Setup Wizard
</h1>
<p className="text-gray-600 mb-6">
Configuration page coming soon...
</p>
<a
href="/"
className="inline-block px-6 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium rounded-lg transition-all"
>
Back to Home
</a>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,16 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};
export default config;

28
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

6148
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "catsave-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": [
"apps/web"
],
"scripts": {
"dev:web": "npm --workspace @catsave/web run dev",
"dev:go": "cd apps/server-go && go run main.go",
"build:web": "npm --workspace @catsave/web run build",
"sync:static": "bash scripts/sync-static.sh",
"build:dev": "npm run build:web && npm run sync:static",
"build:go": "bash scripts/build-go.sh",
"build": "npm run build:web && npm run build:go"
},
"devDependencies": {
"prettier": "^3.3.3"
}
}

66
scripts/build-go.sh Normal file
View File

@@ -0,0 +1,66 @@
#!/bin/bash
echo "🔨 Building CatSave Single Binary..."
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Step 1: Build Next.js static export
echo -e "${BLUE}Step 1: Building Next.js...${NC}"
cd apps/web
npm run build
if [ ! -d "out" ]; then
echo -e "${YELLOW}❌ Next.js build failed${NC}"
exit 1
fi
cd ../..
# Step 2: Copy static files to Go project
echo -e "${BLUE}Step 2: Copying static files to Go...${NC}"
rm -rf apps/server-go/static
mkdir -p apps/server-go/static
cp -r apps/web/out/* apps/server-go/static/
echo " ✅ Copied $(ls -1 apps/server-go/static | wc -l) items"
# Step 3: Build Go binary
echo -e "${BLUE}Step 3: Building Go binary...${NC}"
cd apps/server-go
# Get dependencies
go mod download
# Build for Linux
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ../../dist/catsave-linux main.go
echo " ✅ Built: dist/catsave-linux"
# Build for current OS
go build -ldflags="-s -w" -o ../../dist/catsave main.go
echo " ✅ Built: dist/catsave"
cd ../..
# Get file sizes
SIZE_LINUX=$(du -h dist/catsave-linux 2>/dev/null | cut -f1)
SIZE_LOCAL=$(du -h dist/catsave 2>/dev/null | cut -f1)
echo ""
echo -e "${GREEN}✅ Build complete!${NC}"
echo ""
echo "📦 Output:"
echo " - dist/catsave (${SIZE_LOCAL:-N/A})"
echo " - dist/catsave-linux (${SIZE_LINUX:-N/A})"
echo ""
echo -e "${YELLOW}To run:${NC}"
echo " ./dist/catsave"
echo ""
echo -e "${YELLOW}To deploy to Linux server:${NC}"
echo " scp dist/catsave-linux user@server:/opt/catsave"
echo " ssh user@server '/opt/catsave'"
echo ""

27
scripts/sync-static.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
echo "🔄 Syncing Next.js static files to Go..."
# Check if Next.js build exists
if [ ! -d "apps/web/out" ]; then
echo "❌ Next.js build not found. Run 'npm run build:web' first."
exit 1
fi
# Remove old static files (except .gitkeep if exists)
echo " - Cleaning old files..."
rm -rf apps/server-go/static/*
# Copy new files
echo " - Copying files..."
cp -r apps/web/out/* apps/server-go/static/
# Count files
FILE_COUNT=$(find apps/server-go/static -type f | wc -l)
echo "✅ Synced ${FILE_COUNT} files to apps/server-go/static/"
echo ""
echo "Next steps:"
echo " cd apps/server-go && go run main.go"
echo " Open http://localhost:8080"