Skip to content

Commit

Permalink
Form Testing
Browse files Browse the repository at this point in the history
  • Loading branch information
jasperfurniss committed Jan 9, 2025
1 parent 7533d49 commit df47258
Show file tree
Hide file tree
Showing 12 changed files with 832 additions and 12 deletions.
1 change: 1 addition & 0 deletions examples/playbook-rails/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ GEM

PLATFORMS
arm64-darwin-23
arm64-darwin-24
x86_64-darwin-21
x86_64-darwin-22
x86_64-linux
Expand Down
12 changes: 11 additions & 1 deletion examples/playbook-rails/app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@
@import "playbook";

// Import fonts
@import "fonts"
@import "fonts";


.test {
color: $primary;
font-size: $text_large;
font-weight: $bold;
font-family: $font_family_base;
}


131 changes: 131 additions & 0 deletions examples/playbook-rails/app/assets/stylesheets/feedback_form.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
@import "tokens/colors";
@import "tokens/typography";
@import "tokens/spacing";
@import "tokens/shadows";
@import "tokens/line_height";

.feedback-form {
max-width: 500px;
padding: 24px;
background-color: $card_light;
border: 1px solid $border_light;
border-radius: 8px;
box-shadow: $shadow_deep;

&__title {
font-family: $font_family_base;
font-size: $heading_3;
font-weight: $bold;
color: $text_lt_default;
margin-bottom: 24px;
line-height: $lh_tight;
}

&__field {
margin-bottom: 16px;
}

&__label {
display: block;
font-family: $font_family_base;
font-size: $font_base;
font-weight: $bold;
color: $text_lt_default;
margin-bottom: 8px;
}

&__input {
width: 100%;
padding: 12px;
font-family: $font_family_base;
font-size: $font_base;
color: $text_lt_default;
border: 1px solid $border_light;
border-radius: 4px;
background-color: $white;
transition: border-color 0.2s ease, box-shadow 0.2s ease;

&:focus {
outline: none;
border-color: $focus_color;
box-shadow: 0 0 0 3px $focus_input_light;
}

&--error {
border-color: $error;

&:focus {
box-shadow: 0 0 0 3px $error_subtle;
}
}
}

&__textarea {
@extend .feedback-form__input;
min-height: 120px;
resize: vertical;
}

&__error {
display: none;
color: $error;
font-size: $font_small;
margin-top: 4px;
}

&__error--visible {
display: block;
}

&__submit {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
font-family: $font_family_base;
font-size: $font_base;
font-weight: $bold;
color: $white;
background-color: $primary;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;

&:hover {
background-color: darken($primary, 5%);
}

&:focus {
outline: none;
box-shadow: 0 0 0 3px $focus_input_light;
}

&:disabled {
opacity: 0.7;
cursor: not-allowed;
}

&--loading {
position: relative;
color: transparent;

&::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
border: 2px solid $white;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.8s linear infinite;
}
}
}
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}
25 changes: 25 additions & 0 deletions examples/playbook-rails/app/controllers/feedback_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

class FeedbackController < ApplicationController
skip_before_action :verify_authenticity_token

def create
@feedback = Feedback.new(feedback_params)

if @feedback.save
render json: { message: "Thank you for your feedback!" }, status: :ok
else
render json: {
errors: @feedback.errors.messages.transform_values(&:first),
}, status: :unprocessable_entity
end
rescue
render json: { error: "An unexpected error occurred" }, status: :internal_server_error
end

private

def feedback_params
params.require(:feedback).permit(:name, :comments)
end
end
20 changes: 20 additions & 0 deletions examples/playbook-rails/app/models/feedback.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

class Feedback < ApplicationRecord
# Validations
validates :name, presence: true,
length: { minimum: 2, maximum: 100 }

validates :comments, presence: true,
length: { minimum: 10, maximum: 1000 }

# Sanitization
before_validation :sanitize_fields

private

def sanitize_fields
self.name = name.to_s.strip
self.comments = comments.to_s.strip
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<%= csp_meta_tag %>

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "feedback_form", "data-turbo-track": "reload" %>
</head>

<body>
Expand Down
116 changes: 108 additions & 8 deletions examples/playbook-rails/app/views/pages/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,108 @@
<%= pb_rails("circle_icon_button", props: {
variant: "primary",
icon: "plus"
}) %>
<%= pb_rails("title", props: {
}) do %>
Welcome to Playbook
<% end %>
<div class="feedback-form">
<h2 class="feedback-form__title">Share Your Feedback</h2>

<form action="/feedback" method="post" class="js-feedback-form">
<div class="feedback-form__field">
<label class="feedback-form__label" for="feedback_name">Name</label>
<input type="text" id="feedback_name" name="feedback[name]" class="feedback-form__input" required>
<div class="feedback-form__error" data-error="name">Please enter your name</div>
</div>

<div class="feedback-form__field">
<label class="feedback-form__label" for="feedback_comments">Comments</label>
<textarea id="feedback_comments" name="feedback[comments]" class="feedback-form__textarea" required></textarea>
<div class="feedback-form__error" data-error="comments">Please enter your feedback</div>
</div>

<button type="submit" class="feedback-form__submit">
Submit Feedback
</button>
</form>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('.js-feedback-form');
const submitButton = form.querySelector('.feedback-form__submit');

form.addEventListener('submit', function(e) {
e.preventDefault();

// Reset errors
form.querySelectorAll('.feedback-form__error').forEach(error => {
error.classList.remove('feedback-form__error--visible');
});
form.querySelectorAll('.feedback-form__input, .feedback-form__textarea').forEach(input => {
input.classList.remove('feedback-form__input--error');
});

// Add loading state
submitButton.disabled = true;
submitButton.classList.add('feedback-form__submit--loading');

// Get the form data and structure it properly
const formData = new FormData(form);
const feedbackData = {
feedback: {
name: formData.get('feedback[name]'),
comments: formData.get('feedback[comments]')
}
};

console.log('Sending data:', feedbackData);

// Submit form
fetch(form.action, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(feedbackData)
})
.then(response => {
console.log('Response status:', response.status);
if (!response.ok) {
return response.json().then(data => {
console.log('Error data:', data);
return Promise.reject(data);
});
}
return response.json();
})
.then(data => {
console.log('Success data:', data);
if (data.message) {
alert(data.message);
form.reset();
}
})
.catch(error => {
console.log('Caught error:', error);
if (error.errors) {
Object.entries(error.errors).forEach(([field, message]) => {
if (message) {
const input = form.querySelector(`[name="feedback[${field}]"]`);
if (input) {
input.classList.add('feedback-form__input--error');
const errorElement = form.querySelector(`[data-error="${field}"]`);
if (errorElement) {
errorElement.textContent = message;
errorElement.classList.add('feedback-form__error--visible');
}
}
}
});
} else {
alert('An error occurred. Please try again.');
}
})
.finally(() => {
submitButton.disabled = false;
submitButton.classList.remove('feedback-form__submit--loading');
});
});
});
</script>

9 changes: 6 additions & 3 deletions examples/playbook-rails/config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# frozen_string_literal: true

Rails.application.routes.draw do
root 'pages#index'

get '/pages', to: 'pages#index'
root "pages#index"

get "/pages", to: "pages#index"
post "/feedback", to: "feedback#create"
end
Binary file modified examples/playbook-rails/db/development.sqlite3
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class CreateFeedbacks < ActiveRecord::Migration[7.0]
def change
create_table :feedbacks do |t|
t.string :name, null: false
t.text :comments, null: false
t.timestamps
end
end
end
22 changes: 22 additions & 0 deletions examples/playbook-rails/db/schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2024_01_19_000000) do
create_table "feedbacks", force: :cascade do |t|
t.string "name", null: false
t.text "comments", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
Loading

0 comments on commit df47258

Please sign in to comment.