weeee💃
Some checks failed
Build and Deploy / build (push) Failing after 8s

This commit is contained in:
Mohamad.Elsena 2024-12-27 14:05:56 +01:00
commit 856cb1ee59
36 changed files with 6101 additions and 0 deletions

View 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
View 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
View 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
View File

@ -0,0 +1 @@
/target

1641
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
backend/Cargo.toml Normal file
View 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
View 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

Binary file not shown.

95
backend/src/handlers.rs Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
engine-strict=true

4
frontend/.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

15
frontend/.prettierrc Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

36
frontend/package.json Normal file
View 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
View 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
View 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
View 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
}

View 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>

View 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>

View 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>

View 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>

View 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
View 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;
}

View 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>

View 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>

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
frontend/svelte.config.js Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});