This commit is contained in:
commit
856cb1ee59
34
.gitea/workflows/build_and_deploy.yml
Normal file
34
.gitea/workflows/build_and_deploy.yml
Normal file
@ -0,0 +1,34 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
run: |
|
||||
docker build -t your-dockerhub-username/formies-combined .
|
||||
docker tag your-dockerhub-username/formies-combined:latest
|
||||
docker push your-dockerhub-username/formies-combined:latest
|
||||
|
||||
- name: Deploy to Server (optional)
|
||||
run: |
|
||||
ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} << 'EOF'
|
||||
docker pull your-dockerhub-username/formies-combined:latest
|
||||
docker stop formies || true
|
||||
docker rm formies || true
|
||||
docker run -d --name formies -p 8080:8080 your-dockerhub-username/formies-combined:latest
|
||||
EOF
|
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@ -0,0 +1,51 @@
|
||||
# Stage 1: Build Frontend
|
||||
FROM node:18 as frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
# Copy frontend package files
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the frontend source code
|
||||
COPY frontend ./
|
||||
|
||||
# Build the frontend
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Build Backend
|
||||
FROM rust:1.72 as backend-builder
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
# Copy backend files
|
||||
COPY backend/Cargo.toml backend/Cargo.lock ./
|
||||
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||
RUN cargo build --release
|
||||
|
||||
# Copy the actual backend source code
|
||||
COPY backend/src ./src
|
||||
RUN cargo build --release
|
||||
|
||||
# Stage 3: Combine and Serve
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install dependencies for running Rust binaries
|
||||
RUN apt-get update && apt-get install -y libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy backend binary
|
||||
COPY --from=backend-builder /backend/target/release/backend .
|
||||
|
||||
# Copy frontend static files
|
||||
COPY --from=frontend-builder /frontend/public ./frontend/public
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the backend (serving static files and API)
|
||||
CMD ["./backend"]
|
||||
|
||||
#docker build -t your-dockerhub-username/formies-combined .
|
||||
#docker push your-dockerhub-username/formies-combined:latest
|
221
README.md
Normal file
221
README.md
Normal file
@ -0,0 +1,221 @@
|
||||
# Formies
|
||||
|
||||
Formies is a form management tool that allows you to create customizable forms, collect submissions, and view collected data. This project combines a Rust backend and a Svelte frontend, packaged as a single Docker container for easy deployment.
|
||||
|
||||
## Features
|
||||
|
||||
### 📝 Form Management
|
||||
|
||||
- Create forms with customizable fields (text, number, date, etc.).
|
||||
- View all created forms in a centralized dashboard.
|
||||
|
||||
### 🗂️ Submissions
|
||||
|
||||
- Submit responses to forms via a user-friendly interface.
|
||||
- View and manage all form submissions.
|
||||
|
||||
### ⚙️ Backend
|
||||
|
||||
- Built with Rust using Actix-Web for high performance and scalability.
|
||||
- Uses SQLite for local data storage with easy migration to PostgreSQL if needed.
|
||||
|
||||
### 🎨 Frontend
|
||||
|
||||
- Built with SvelteKit for a modern and lightweight user experience.
|
||||
- Responsive design for use across devices.
|
||||
|
||||
### 🚀 Deployment
|
||||
|
||||
- Packaged as a single Docker image for seamless deployment.
|
||||
- Supports CI/CD workflows with Gitea Actions, Drone CI, or GitHub Actions.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── backend/ # Backend codebase
|
||||
│ ├── src/
|
||||
│ │ ├── handlers.rs # Route handlers for Actix-Web
|
||||
│ │ ├── models.rs # Data models (Form, Submission)
|
||||
│ │ ├── db.rs # Database initialization
|
||||
│ │ ├── main.rs # Main entry point for the backend
|
||||
│ │ └── ... # Additional modules
|
||||
│ ├── Cargo.toml # Backend dependencies
|
||||
│ └── Cargo.lock # Locked dependencies
|
||||
│
|
||||
├── frontend/ # Frontend codebase
|
||||
│ ├── public/ # Built static files (after `npm run build`)
|
||||
│ ├── src/
|
||||
│ │ ├── lib/ # Shared utilities (e.g., API integration)
|
||||
│ │ ├── routes/ # Svelte pages
|
||||
│ │ │ ├── +page.svelte # Dashboard
|
||||
│ │ │ └── form/ # Form-related pages
|
||||
│ │ └── main.ts # Frontend entry point
|
||||
│ ├── package.json # Frontend dependencies
|
||||
│ ├── svelte.config.js # Svelte configuration
|
||||
│ └── ... # Additional files
|
||||
│
|
||||
├── Dockerfile # Combined Dockerfile for both frontend and backend
|
||||
├── docker-compose.yml # Docker Compose file for deployment
|
||||
├── .gitea/ # Gitea Actions workflows
|
||||
│ └── workflows/
|
||||
│ └── build_and_deploy.yml
|
||||
├── .drone.yml # Drone CI configuration
|
||||
├── README.md # Documentation (this file)
|
||||
└── ... # Other configuration files
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Docker
|
||||
|
||||
- Install Docker: [Docker Documentation](https://docs.docker.com/)
|
||||
|
||||
### Rust (for development)
|
||||
|
||||
- Install Rust: [Rustup Installation](https://rustup.rs/)
|
||||
|
||||
### Node.js (for frontend development)
|
||||
|
||||
- Install Node.js: [Node.js Downloads](https://nodejs.org/)
|
||||
|
||||
## Development
|
||||
|
||||
### Backend
|
||||
|
||||
1. Navigate to the backend/ directory:
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
```
|
||||
|
||||
2. Run the backend server:
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
The backend will be available at [http://localhost:8080](http://localhost:8080).
|
||||
|
||||
### Frontend
|
||||
|
||||
1. Navigate to the frontend/ directory:
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The frontend will be available at [http://localhost:5173](http://localhost:5173).
|
||||
|
||||
## Deployment
|
||||
|
||||
### Build the Docker Image
|
||||
|
||||
1. Build the combined Docker image:
|
||||
|
||||
```sh
|
||||
docker build -t your-dockerhub-username/formies-combined .
|
||||
```
|
||||
|
||||
2. Run the Docker container:
|
||||
|
||||
```sh
|
||||
docker run -p 8080:8080 your-dockerhub-username/formies-combined
|
||||
```
|
||||
|
||||
Access the application at [http://localhost:8080](http://localhost:8080).
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
1. Deploy using `docker-compose.yml`:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. Stop the containers:
|
||||
```sh
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## CI/CD Workflow
|
||||
|
||||
### Gitea Actions
|
||||
|
||||
1. Create a file at `.gitea/workflows/build_and_deploy.yml`:
|
||||
|
||||
```yaml
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
run: |
|
||||
docker build -t your-dockerhub-username/formies-combined .
|
||||
docker push your-dockerhub-username/formies-combined:latest
|
||||
|
||||
- name: Deploy to Server (optional)
|
||||
run: |
|
||||
ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} << 'EOF'
|
||||
docker pull your-dockerhub-username/formies-combined:latest
|
||||
docker stop formies || true
|
||||
docker rm formies || true
|
||||
docker run -d --name formies -p 8080:8080 your-dockerhub-username/formies-combined:latest
|
||||
EOF
|
||||
```
|
||||
|
||||
2. Add secrets in Gitea:
|
||||
- `DOCKER_USERNAME`: Your Docker Hub username.
|
||||
- `DOCKER_PASSWORD`: Your Docker Hub password.
|
||||
- `SERVER_USER`: SSH username for deployment.
|
||||
- `SERVER_IP`: IP address of the server.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
**Base URL:** `http://localhost:8080`
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| ------ | ----------------------------- | ----------------------------------- |
|
||||
| POST | `/api/forms` | Create a new form |
|
||||
| GET | `/api/forms` | Get all forms |
|
||||
| POST | `/api/forms/{id}/submissions` | Submit data to a form |
|
||||
| GET | `/api/forms/{id}/submissions` | Get submissions for a specific form |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Authentication:** Add user-based authentication for managing forms and submissions.
|
||||
- **Export:** Allow exporting submissions to CSV or Excel.
|
||||
- **Scaling:** Migrate to PostgreSQL for distributed data handling.
|
||||
- **Monitoring:** Integrate tools like Prometheus and Grafana for performance monitoring.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
|
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1641
backend/Cargo.lock
generated
Normal file
1641
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
backend/Cargo.toml
Normal file
12
backend/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "formies_be"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0"
|
||||
rusqlite = "0.29"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
actix-files = "0.6"
|
28
backend/src/db.rs
Normal file
28
backend/src/db.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use rusqlite::{Connection, Result};
|
||||
|
||||
pub fn init_db() -> Result<Connection> {
|
||||
let conn = Connection::open("form_data.db")?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS forms (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
fields TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS submissions (
|
||||
id TEXT PRIMARY KEY,
|
||||
form_id TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (form_id) REFERENCES forms (id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
BIN
backend/src/form_data.db
Normal file
BIN
backend/src/form_data.db
Normal file
Binary file not shown.
95
backend/src/handlers.rs
Normal file
95
backend/src/handlers.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use rusqlite::{params, Connection};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{Form, Submission};
|
||||
|
||||
// Create a new form
|
||||
pub async fn create_form(
|
||||
db: web::Data<Arc<Mutex<Connection>>>,
|
||||
form: web::Json<Form>,
|
||||
) -> impl Responder {
|
||||
let conn = db.lock().unwrap(); // Lock the Mutex to access the database
|
||||
let form_id = Uuid::new_v4().to_string();
|
||||
let form_json = serde_json::to_string(&form.fields).unwrap();
|
||||
|
||||
match conn.execute(
|
||||
"INSERT INTO forms (id, name, fields) VALUES (?1, ?2, ?3)",
|
||||
params![form_id, form.name, form_json],
|
||||
) {
|
||||
Ok(_) => HttpResponse::Ok().json(form_id),
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Get all forms
|
||||
pub async fn get_forms(db: web::Data<Arc<Mutex<Connection>>>) -> impl Responder {
|
||||
let conn = db.lock().unwrap(); // Lock the Mutex to access the database
|
||||
|
||||
let mut stmt = match conn.prepare("SELECT id, name, fields FROM forms") {
|
||||
Ok(stmt) => stmt,
|
||||
Err(e) => return HttpResponse::InternalServerError().body(format!("Error: {}", e)),
|
||||
};
|
||||
|
||||
let forms_iter = stmt
|
||||
.query_map([], |row| {
|
||||
let id: String = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
let fields: String = row.get(2)?;
|
||||
let fields = serde_json::from_str(&fields).unwrap();
|
||||
Ok(Form { id, name, fields })
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let forms: Vec<Form> = forms_iter.filter_map(|f| f.ok()).collect();
|
||||
HttpResponse::Ok().json(forms)
|
||||
}
|
||||
|
||||
// Submit a form
|
||||
pub async fn submit_form(
|
||||
db: web::Data<Arc<Mutex<Connection>>>,
|
||||
path: web::Path<String>,
|
||||
submission: web::Json<serde_json::Value>,
|
||||
) -> impl Responder {
|
||||
let conn = db.lock().unwrap(); // Lock the Mutex to access the database
|
||||
let submission_id = Uuid::new_v4().to_string();
|
||||
let form_id = path.into_inner();
|
||||
let submission_json = serde_json::to_string(&submission.into_inner()).unwrap();
|
||||
|
||||
match conn.execute(
|
||||
"INSERT INTO submissions (id, form_id, data) VALUES (?1, ?2, ?3)",
|
||||
params![submission_id, form_id, submission_json],
|
||||
) {
|
||||
Ok(_) => HttpResponse::Ok().json(submission_id),
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Get submissions for a form
|
||||
pub async fn get_submissions(
|
||||
db: web::Data<Arc<Mutex<Connection>>>,
|
||||
path: web::Path<String>,
|
||||
) -> impl Responder {
|
||||
let conn = db.lock().unwrap(); // Lock the Mutex to access the database
|
||||
let form_id = path.into_inner();
|
||||
|
||||
let mut stmt =
|
||||
match conn.prepare("SELECT id, form_id, data FROM submissions WHERE form_id = ?1") {
|
||||
Ok(stmt) => stmt,
|
||||
Err(e) => return HttpResponse::InternalServerError().body(format!("Error: {}", e)),
|
||||
};
|
||||
|
||||
let submissions_iter = stmt
|
||||
.query_map([form_id], |row| {
|
||||
let id: String = row.get(0)?;
|
||||
let form_id: String = row.get(1)?;
|
||||
let data: String = row.get(2)?;
|
||||
let data = serde_json::from_str(&data).unwrap();
|
||||
Ok(Submission { id, form_id, data })
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let submissions: Vec<Submission> = submissions_iter.filter_map(|s| s.ok()).collect();
|
||||
HttpResponse::Ok().json(submissions)
|
||||
}
|
35
backend/src/main.rs
Normal file
35
backend/src/main.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use actix_files as fs;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
mod db;
|
||||
mod handlers;
|
||||
mod models;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Initialize the database connection
|
||||
let db = Arc::new(Mutex::new(
|
||||
db::init_db().expect("Failed to initialize the database"),
|
||||
));
|
||||
|
||||
// Start the Actix-Web server
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(db.clone()))
|
||||
.service(fs::Files::new("/", "./frontend/public").index_file("index.html"))
|
||||
.route("/forms", web::post().to(handlers::create_form))
|
||||
.route("/forms", web::get().to(handlers::get_forms))
|
||||
.route(
|
||||
"/forms/{id}/submissions",
|
||||
web::post().to(handlers::submit_form),
|
||||
)
|
||||
.route(
|
||||
"/forms/{id}/submissions",
|
||||
web::get().to(handlers::get_submissions),
|
||||
)
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
15
backend/src/models.rs
Normal file
15
backend/src/models.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Form {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub fields: serde_json::Value, // JSON array of form fields
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Submission {
|
||||
pub id: String,
|
||||
pub form_id: String,
|
||||
pub data: serde_json::Value, // JSON of submission data
|
||||
}
|
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@ -0,0 +1,9 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
formies:
|
||||
image: your-dockerhub-username/formies-combined:latest
|
||||
container_name: formies-app
|
||||
ports:
|
||||
- "8080:8080" # Expose the application on port 8080
|
||||
restart: always
|
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
engine-strict=true
|
4
frontend/.prettierignore
Normal file
4
frontend/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
15
frontend/.prettierrc
Normal file
15
frontend/.prettierrc
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
38
frontend/README.md
Normal file
38
frontend/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
34
frontend/eslint.config.js
Normal file
34
frontend/eslint.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import js from '@eslint/js';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript-eslint';
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default ts.config(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
3423
frontend/package-lock.json
generated
Normal file
3423
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
frontend/package.json
Normal file
36
frontend/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "formies-fe",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.3",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"globals": "^15.0.0",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/uuid": "^10.0.0",
|
||||
"axios": "^1.7.9"
|
||||
}
|
||||
}
|
13
frontend/src/app.d.ts
vendored
Normal file
13
frontend/src/app.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
frontend/src/app.html
Normal file
12
frontend/src/app.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
28
frontend/src/lib/api.ts
Normal file
28
frontend/src/lib/api.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import axios from 'axios';
|
||||
import type { Form, Submission } from './types';
|
||||
|
||||
const API_BASE = 'http://localhost:8080'; // Backend URL
|
||||
|
||||
// Fetch all forms
|
||||
export async function fetchForms(): Promise<Form[]> {
|
||||
const response = await axios.get(`${API_BASE}/forms`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Create a new form
|
||||
export async function createForm(form: Omit<Form, 'id' | 'created_at'>): Promise<string> {
|
||||
const response = await axios.post(`${API_BASE}/forms`, form);
|
||||
return response.data; // Returns the created form's ID
|
||||
}
|
||||
|
||||
// Fetch form submissions
|
||||
export async function fetchSubmissions(formId: string): Promise<Submission[]> {
|
||||
const response = await axios.get(`${API_BASE}/forms/${formId}/submissions`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Submit a form
|
||||
export async function submitForm(formId: string, data: Record<string, unknown>): Promise<string> {
|
||||
const response = await axios.post(`${API_BASE}/forms/${formId}/submissions`, data);
|
||||
return response.data; // Returns the submission ID
|
||||
}
|
32
frontend/src/lib/components/Dashboard.svelte
Normal file
32
frontend/src/lib/components/Dashboard.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import axios from 'axios';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let forms: any = [];
|
||||
|
||||
onMount(async () => {
|
||||
const response = await axios.get('http://localhost:8080/forms');
|
||||
forms = response.data;
|
||||
});
|
||||
|
||||
function viewForm(id: number) {
|
||||
goto(`/form/${id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>Forms Dashboard</h1>
|
||||
{#if forms.length === 0}
|
||||
<p>No forms available.</p>
|
||||
{/if}
|
||||
|
||||
<ul>
|
||||
{#each forms as form}
|
||||
<li>
|
||||
<h3>{form.name}</h3>
|
||||
<button on:click={() => viewForm(form.id)}>View</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
44
frontend/src/lib/components/FormBuilder.svelte
Normal file
44
frontend/src/lib/components/FormBuilder.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import axios from 'axios';
|
||||
|
||||
let formName = '';
|
||||
/**
|
||||
* @type {any[]}
|
||||
*/
|
||||
let fields = [];
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
*/
|
||||
function addField(type) {
|
||||
fields.push({ label: '', name: '', type });
|
||||
}
|
||||
|
||||
async function saveForm() {
|
||||
const response = await axios.post('http://localhost:8080/forms', {
|
||||
name: formName,
|
||||
fields
|
||||
});
|
||||
alert(`Form saved with ID: ${response.data}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>Create a Form</h1>
|
||||
<input type="text" bind:value={formName} placeholder="Form Name" />
|
||||
|
||||
<button on:click={() => addField('text')}>Add Text Field</button>
|
||||
<button on:click={() => addField('number')}>Add Number Field</button>
|
||||
|
||||
{#each fields as field, index}
|
||||
<div>
|
||||
<input type="text" bind:value={field.label} placeholder="Field Label" />
|
||||
<input type="text" bind:value={field.name} placeholder="Field Name" />
|
||||
<span>{field.type}</span>
|
||||
<button on:click={() => fields.splice(index, 1)}>Remove</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<button on:click={saveForm}>Save Form</button>
|
||||
</div>
|
35
frontend/src/lib/components/FormRenderer.svelte
Normal file
35
frontend/src/lib/components/FormRenderer.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script>
|
||||
// @ts-nocheck
|
||||
|
||||
export let form;
|
||||
/**
|
||||
* @type {(arg0: {}) => void}
|
||||
*/
|
||||
export let onSubmit;
|
||||
|
||||
let formData = {};
|
||||
|
||||
/**
|
||||
* @param {{ preventDefault: () => void; }} e
|
||||
*/
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit={handleSubmit}>
|
||||
{#each form.fields as field}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label>{field.label}</label>
|
||||
{#if field.type === 'text'}
|
||||
<input type="text" bind:value={formData[field.name]} />
|
||||
{:else if field.type === 'number'}
|
||||
<input type="number" bind:value={formData[field.name]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
32
frontend/src/lib/components/Routes.svelte
Normal file
32
frontend/src/lib/components/Routes.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import axios from 'axios';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let forms: any = [];
|
||||
|
||||
onMount(async () => {
|
||||
const response = await axios.get('http://localhost:8080/forms');
|
||||
forms = response.data;
|
||||
});
|
||||
|
||||
function viewForm(id: number) {
|
||||
goto(`/form/${id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>Forms Dashboard</h1>
|
||||
{#if forms.length === 0}
|
||||
<p>No forms available.</p>
|
||||
{/if}
|
||||
|
||||
<ul>
|
||||
{#each forms as form}
|
||||
<li>
|
||||
<h3>{form.name}</h3>
|
||||
<button on:click={() => viewForm(form.id)}>View</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
1
frontend/src/lib/index.ts
Normal file
1
frontend/src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
19
frontend/src/lib/types.ts
Normal file
19
frontend/src/lib/types.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface FormField {
|
||||
label: string;
|
||||
name: string;
|
||||
field_type: 'text' | 'number' | 'date' | 'textarea';
|
||||
}
|
||||
|
||||
export interface Form {
|
||||
id: string;
|
||||
name: string;
|
||||
fields: FormField[];
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface Submission {
|
||||
id: string;
|
||||
form_id: string;
|
||||
data: Record<string, unknown>;
|
||||
created_at?: string;
|
||||
}
|
23
frontend/src/routes/+page.svelte
Normal file
23
frontend/src/routes/+page.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fetchForms } from '../lib/api';
|
||||
import type { Form } from '../lib/types';
|
||||
|
||||
let forms: Form[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
forms = await fetchForms();
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1>Form Management Tool</h1>
|
||||
|
||||
<a href="/create">Create a New Form</a>
|
||||
|
||||
<ul>
|
||||
{#each forms as form}
|
||||
<li>
|
||||
<a href={`/form/${form.id}`}>{form.name}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
50
frontend/src/routes/create/+page.svelte
Normal file
50
frontend/src/routes/create/+page.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { createForm } from '../../lib/api';
|
||||
import type { FormField } from '../../lib/types';
|
||||
|
||||
let name = '';
|
||||
let fields: FormField[] = [];
|
||||
|
||||
function addField() {
|
||||
fields.push({ label: '', name: '', field_type: 'text' });
|
||||
}
|
||||
|
||||
async function saveForm() {
|
||||
await createForm({ name, fields });
|
||||
alert('Form created successfully!');
|
||||
location.href = '/';
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1>Create Form</h1>
|
||||
|
||||
<label>
|
||||
Form Name:
|
||||
<input type="text" bind:value={name} />
|
||||
</label>
|
||||
|
||||
<h2>Fields</h2>
|
||||
{#each fields as field, i}
|
||||
<div>
|
||||
<label>
|
||||
Label:
|
||||
<input type="text" bind:value={field.label} />
|
||||
</label>
|
||||
<label>
|
||||
Name:
|
||||
<input type="text" bind:value={field.name} />
|
||||
</label>
|
||||
<label>
|
||||
Type:
|
||||
<select bind:value={field.field_type}>
|
||||
<option value="text">Text</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="textarea">Textarea</option>
|
||||
</select>
|
||||
</label>
|
||||
<button on:click={() => fields.splice(i, 1)}>Remove</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button on:click={addField}>Add Field</button>
|
||||
<button on:click={saveForm}>Save Form</button>
|
53
frontend/src/routes/form/[id]/+page.svelte
Normal file
53
frontend/src/routes/form/[id]/+page.svelte
Normal file
@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fetchForms, fetchSubmissions, submitForm } from '../../../lib/api';
|
||||
import type { Form, Submission } from '../../../lib/types';
|
||||
|
||||
export let params: { id: string };
|
||||
let form: Form | null = null;
|
||||
let submissions: Submission[] = [];
|
||||
let responseData: Record<string, any> = {};
|
||||
|
||||
onMount(async () => {
|
||||
form = await fetchForms().then((forms) => forms.find((f) => f.id === params.id) || null);
|
||||
submissions = await fetchSubmissions(params.id);
|
||||
});
|
||||
|
||||
async function submitResponse() {
|
||||
await submitForm(params.id, responseData);
|
||||
alert('Response submitted successfully!');
|
||||
submissions = await fetchSubmissions(params.id); // Refresh submissions
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1>{form?.name}</h1>
|
||||
|
||||
{#if form}
|
||||
<form on:submit|preventDefault={submitResponse}>
|
||||
{#each form.fields as field}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label>{field.label}</label>
|
||||
{#if field.field_type === 'text'}
|
||||
<input type="text" bind:value={responseData[field.name]} />
|
||||
{:else if field.field_type === 'number'}
|
||||
<input type="number" bind:value={responseData[field.name]} />
|
||||
{:else if field.field_type === 'date'}
|
||||
<input type="date" bind:value={responseData[field.name]} />
|
||||
{:else if field.field_type === 'textarea'}
|
||||
<textarea bind:value={responseData[field.name]}></textarea>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<h2>Submissions</h2>
|
||||
<ul>
|
||||
{#each submissions as submission}
|
||||
<li>{JSON.stringify(submission.data)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
BIN
frontend/static/favicon.png
Normal file
BIN
frontend/static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
18
frontend/svelte.config.js
Normal file
18
frontend/svelte.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
19
frontend/tsconfig.json
Normal file
19
frontend/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
6
frontend/vite.config.ts
Normal file
6
frontend/vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
Loading…
Reference in New Issue
Block a user