diff --git a/app/components/npq_separation/timeline_component.rb b/app/components/npq_separation/timeline_component.rb
new file mode 100644
index 0000000000..378e73efe7
--- /dev/null
+++ b/app/components/npq_separation/timeline_component.rb
@@ -0,0 +1,61 @@
+module NpqSeparation
+ class TimelineComponent < ViewComponent::Base
+ renders_many :items, "ItemComponent"
+
+ attr_reader :events
+
+ def initialize(events)
+ @events = events.sort_by(&:created_at)
+
+ @events.each { |event| with_item(event) }
+ end
+
+ def call
+ tag.div(class: "app-timeline") do
+ safe_join(items)
+ end
+ end
+
+ class ItemComponent < ViewComponent::Base
+ attr_reader :event
+
+ def initialize(event)
+ @event = event
+ end
+
+ def call
+ tag.div(class: "app-timeline__item") do
+ safe_join([header, timestamp, description])
+ end
+ end
+
+ private
+
+ def header
+ tag.div(class: "app-timeline__header") do
+ safe_join([title, byline], " ")
+ end
+ end
+
+ def title
+ tag.h2(event.title, class: "app-timeline__title")
+ end
+
+ def timestamp
+ tag.p(class: "app-timeline__date") do
+ tag.time(event.created_at.to_fs(:govuk_short), datetime: event.created_at.to_fs(:iso8601))
+ end
+ end
+
+ def description
+ tag.div(class: "app-timeline__description") { event.description }
+ end
+
+ def byline
+ return if event.byline.blank?
+
+ tag.p("by #{event.byline}", class: "app-timeline__byline")
+ end
+ end
+ end
+end
diff --git a/app/views/npq_separation/admin/applications/show.html.erb b/app/views/npq_separation/admin/applications/show.html.erb
index 2fe33cf3a2..f738a524bc 100644
--- a/app/views/npq_separation/admin/applications/show.html.erb
+++ b/app/views/npq_separation/admin/applications/show.html.erb
@@ -83,3 +83,7 @@
end
end
%>
+
+
Timeline
+
+<%= render NpqSeparation::TimelineComponent.new(@application.events) %>
diff --git a/app/webpacker/styles/application.scss b/app/webpacker/styles/application.scss
index ee5ecc0d89..30ce72f405 100644
--- a/app/webpacker/styles/application.scss
+++ b/app/webpacker/styles/application.scss
@@ -16,6 +16,7 @@ $govuk-image-url-function: frontend-image-url;
@import "big-number";
@import "admin";
@import "api_guidance";
+@import "timeline";
h1.govuk-fieldset__heading {
margin-bottom: 10px;
diff --git a/app/webpacker/styles/timeline.scss b/app/webpacker/styles/timeline.scss
new file mode 100644
index 0000000000..8ea0ac0622
--- /dev/null
+++ b/app/webpacker/styles/timeline.scss
@@ -0,0 +1,107 @@
+// timeline
+//
+// borrowed from the MoJ frontend library, moj- replaced with app- as
+// we will likely customise this to suit our needs
+//
+// https://design-patterns.service.justice.gov.uk/components/timeline/
+.app-timeline {
+ margin-bottom: govuk-spacing(4);
+ overflow: hidden;
+ position: relative;
+
+ &:before {
+ background-color: $govuk-brand-colour;
+ content: "";
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: govuk-spacing(2);
+ width: 5px;
+ }
+
+}
+
+.app-timeline--full {
+ margin-bottom: 0;
+ &:before {
+ height: calc(100% - 75px);
+ }
+}
+
+.app-timeline__item {
+ padding-bottom: govuk-spacing(6);
+ padding-left: govuk-spacing(4);
+ position: relative;
+
+ &:before {
+ background-color: $govuk-brand-colour;
+ content: "";
+ height: 5px;
+ left: 0;
+ position: absolute;
+ top: govuk-spacing(2);
+ width: 15px;
+ }
+
+}
+
+.app-timeline__title {
+ @include govuk-font($size: 19, $weight: bold);
+ display: inline;
+}
+
+.app-timeline__byline {
+ @include govuk-font($size: 19);
+ color: $govuk-secondary-text-colour;
+ display: inline;
+ margin: 0;
+}
+
+.app-timeline__date {
+ @include govuk-font($size: 16);
+ margin-top: govuk-spacing(1);
+ margin-bottom: 0;
+}
+
+.app-timeline__description {
+ @include govuk-font($size: 19);
+ margin-top: govuk-spacing(4);
+}
+
+.app-timeline__documents {
+ list-style: none;
+ margin-bottom: 0;
+ padding-left: 0;
+}
+
+.app-timeline__document-item {
+ margin-bottom: govuk-spacing(1);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+}
+
+.app-timeline__document-icon {
+ float: left;
+ margin-top: 4px;
+ margin-right: 4px;
+ fill: currentColor;
+
+ @media screen and (forced-colors: active) {
+ fill: linkText;
+ }
+}
+
+.app-timeline__document-link {
+ // background-image: url(#{$app-images-path}icon-document.svg);
+ background-repeat: no-repeat;
+ background-size: 20px 16px;
+ background-position: 0 50%;
+ padding-left: govuk-spacing(5);
+
+ &:focus {
+ color: govuk-colour("black"); // Focus colour on yellow should really be black.
+ }
+}
diff --git a/spec/components/npq_separation/timeline_component_spec.rb b/spec/components/npq_separation/timeline_component_spec.rb
new file mode 100644
index 0000000000..c3f3181f31
--- /dev/null
+++ b/spec/components/npq_separation/timeline_component_spec.rb
@@ -0,0 +1,36 @@
+require "rails_helper"
+
+RSpec.describe NpqSeparation::TimelineComponent, type: :component do
+ let(:one_day_ago) { FactoryBot.build(:event, :with_byline, created_at: 1.day.ago) }
+ let(:two_days_ago) { FactoryBot.build(:event, :with_byline, created_at: 2.days.ago) }
+ let(:three_days_ago) { FactoryBot.build(:event, :with_byline, created_at: 3.days.ago) }
+ let(:events) { [two_days_ago, one_day_ago, three_days_ago] }
+
+ subject { NpqSeparation::TimelineComponent.new(events) }
+
+ before { render_inline(subject) }
+
+ context "when the events aren't in choronological order" do
+ it "orders the events by created_at on initialization" do
+ expect(subject.events).to eql(events.sort_by(&:created_at))
+ end
+ end
+
+ it "displays all of the events in a timeline" do
+ expect(rendered_content).to have_css(".app-timeline__item", count: events.size)
+ end
+
+ it "shows a timestamp for each event" do
+ events.each do |event|
+ expect(rendered_content).to have_css("time", text: event.created_at.to_fs(:govuk_short))
+ expect(rendered_content).to have_css("time[datetime='#{event.created_at.to_fs(:iso8601)}']")
+ end
+ end
+
+ it "shows the title and byline in the header" do
+ events.each do |event|
+ expect(rendered_content).to have_css(".app-timeline__header > .app-timeline__title", text: event.title)
+ expect(rendered_content).to have_css(".app-timeline__header > .app-timeline__byline", text: event.byline)
+ end
+ end
+end
diff --git a/spec/factories/event.rb b/spec/factories/event.rb
new file mode 100644
index 0000000000..0b38e210fc
--- /dev/null
+++ b/spec/factories/event.rb
@@ -0,0 +1,10 @@
+FactoryBot.define do
+ factory :event do
+ sequence(:title) { |n| "Event #{n}" }
+ sequence(:description) { |n| "Event #{n} description goes here" }
+ created_at { Time.zone.now }
+
+ trait(:with_random_description) { description { Faker::Lorem.paragraph } }
+ trait(:with_byline) { byline { Faker::Name.name } }
+ end
+end