diff --git a/README.md b/README.md index c11abf4..f6db69d 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,6 @@ A lightweight application for generating dynamic form websites and storing user responses. The form fields, labels, and behavior are fully configurable via a TOML configuration file. -## Usage - -The server will listen on `127.0.0.1:8081` by default. You can override with: - -```bash -SERVER_PORT=8082 form-generator -``` - ## Configuration The application is configured via a TOML file (default: `config.toml`). @@ -20,13 +12,15 @@ See [config.toml](./config.toml) for an example. * `json_output`: Path to the JSON file where responses are stored. +* `form_title`: Title of the page and form. + * `submit_button`: Text displayed on the form’s submit button. * `fields`: List of form fields * `name`: Internal key for storage. * `title`: Label displayed in the form. * `description`: Short description displayed below the field. - * `answer_type`: Type of input (`text`, `number`, `email`, `password`, `url`, `tel`, `textarea`). + * `answer_type`: Type of input (`text`, `number`, `email`, `password`, `url`, `tel`, `textarea`, `select`, `checkbox`, etc). * `html_before` (optional): HTML snippet rendered before the field. * `html_after` (optional): HTML snippet rendered after the field. diff --git a/answers.json b/answers.json deleted file mode 100644 index 9fd3e63..0000000 --- a/answers.json +++ /dev/null @@ -1,54 +0,0 @@ -[ - { - "timestamp": "2026-02-19T12:46:14.811307031Z", - "answers": { - "experience_level": "", - "full_name": "", - "age": "", - "newsletter": "true", - "about_you": "", - "comments": "", - "department": "Design", - "contact_number": "", - "password": "!Y%5#yH^QcZmwHR", - "favorite_color": "", - "email": "foobar@mail.com", - "website": "" - } - }, - { - "timestamp": "2026-02-19T12:46:37.950223451Z", - "answers": { - "full_name": "", - "contact_number": "", - "website": "", - "favorite_color": "", - "age": "", - "password": "!Y%5#yH^QcZmwHR", - "department": "Design", - "comments": "", - "experience_level": "Mid-level", - "newsletter": "true", - "about_you": "", - "email": "foobar@mail.com" - } - }, - { - "timestamp": "2026-02-19T12:50:56.697577391Z", - "answers": { - "website": "https://asdf.com", - "password": "!Y%5#yH^QcZmwHR", - "favorite_color": "aasdf", - "about_you": "asdf", - "terms": "true", - "comments": "asdfasdf", - "age": "3", - "contact_number": "asdf", - "full_name": "babro", - "newsletter": "true", - "department": "Engineering", - "email": "foobar@mail.com", - "experience_level": "Junior" - } - } -] \ No newline at end of file diff --git a/src/handlers.rs b/src/handlers.rs index 44e7497..a9e0d42 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -20,10 +20,28 @@ pub struct FieldDef { pub options: Option>, } +pub enum FieldWidget<'a> { + Checkbox, + Textarea, + Select(&'a [String]), + Input(&'a str), +} + +impl FieldDef { + pub fn widget(&self) -> FieldWidget<'_> { + match self.answer_type.as_str() { + "checkbox" => FieldWidget::Checkbox, + "textarea" => FieldWidget::Textarea, + "select" => FieldWidget::Select(self.options.as_deref().unwrap_or(&[])), + other => FieldWidget::Input(other), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] struct ResponseEntry { timestamp: chrono::DateTime, - answers: HashMap, + answers: HashMap>, } #[derive(Debug, Deserialize)] @@ -65,11 +83,27 @@ pub async fn render_form(State(state): State) -> impl IntoResponse { pub async fn submit( State(state): State, - Form(form): Form>, + Form(mut form): Form>, ) -> impl IntoResponse { + let mut processed_answers = HashMap::new(); + + for field in &state.cfg.fields { + let value = form + .remove(&field.name) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + + processed_answers.insert(field.name.clone(), value); + } + + for (key, val) in form { + let value = Some(val.trim().to_string()).filter(|s| !s.is_empty()); + processed_answers.insert(key, value); + } + let entry = ResponseEntry { timestamp: Utc::now(), - answers: form, + answers: processed_answers, }; let _guard = state.file_lock.lock().await; diff --git a/src/main.rs b/src/main.rs index 327f126..d36ca06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,12 @@ struct Cli { #[arg(short, long)] output_file: Option, + #[arg(short, long, default_value = "8081")] + port: u16, + + #[arg(short = 'H', long)] + host: bool, + #[arg(short, long)] verbose: bool, } @@ -55,7 +61,7 @@ async fn main() -> anyhow::Result<()> { // rate limiter let governor_conf = GovernorConfigBuilder::default() - .per_second(3) + .per_second(1) .burst_size(10) .finish() .unwrap(); @@ -81,8 +87,11 @@ async fn main() -> anyhow::Result<()> { .layer(cors) .layer(GovernorLayer::new(governor_conf)); - let port = std::env::var("SERVER_PORT").unwrap_or("8081".to_string()); - let addr = format!("0.0.0.0:{port}"); + let addr = if cli.host { + format!("0.0.0.0:{}", cli.port) + } else { + format!("127.0.0.1:{}", cli.port) + }; let listener = tokio::net::TcpListener::bind(&addr).await?; tracing::info!("Listening on {}", addr); diff --git a/templates/_layout.css b/templates/_layout.css index 8169493..97765ee 100644 --- a/templates/_layout.css +++ b/templates/_layout.css @@ -69,6 +69,29 @@ h1 { width: 100%; } +.checkbox-label { + display: flex; + align-items: flex-start; + gap: 12px; + cursor: pointer; + margin: 0; +} + +.label-content { + display: flex; + flex-direction: column; +} + +.label-title { + font-weight: 600; + font-size: 0.95rem; + color: var(--text-main); +} + +.checkbox-desc { + margin: 4px 0 0 0; +} + label { display: block; cursor: pointer; diff --git a/templates/form.html b/templates/form.html index 5601c35..e6dc558 100644 --- a/templates/form.html +++ b/templates/form.html @@ -7,50 +7,62 @@

{{ form_title }}

- {% for f in fields %} - {% if f.html_before.is_some() %} - {{ f.html_before.as_ref().unwrap() | safe }} + {% for f in fields %} + + {% if let Some(html) = f.html_before %} + {{ html | safe }} {% endif %}
- {% if f.answer_type == "checkbox" %} - - {% else %} - + {% match f.widget() %} - {% if f.answer_type == "textarea" %} - + {% when FieldWidget::Checkbox %} + - {% elif f.answer_type == "select" %} - + {% when FieldWidget::Textarea %} + + - {% else %} - - {% endif %} - {% endif %} + {% when FieldWidget::Select with (options) %} + + + + {% when FieldWidget::Input with (input_type) %} + + + + {% endmatch %}
- {% if f.html_after.is_some() %} - {{ f.html_after.as_ref().unwrap() | safe }} + {% if let Some(html) = f.html_after %} + {{ html | safe }} {% endif %} - {% endfor %} -
+ + {% endfor %} + +
+ +
{% endblock %}