Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/controllers/site/credentials_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Site
class CredentialsController < BaseController
before_action :set_award

def show
end

def badge
@theme = (params[:theme] == "dark") ? "dark" : "light"
response.headers["Content-Disposition"] = %(inline; filename="#{@award.definition.slug}-badge-#{@theme}.svg")
render formats: :svg, content_type: "image/svg+xml"
end

def certificate
response.headers["Content-Disposition"] = %(attachment; filename="#{@award.definition.slug}-#{@award.public_id}.svg")
render formats: :svg, content_type: "image/svg+xml"
end

private

def set_award
@award = ::Credentials::Award.publicly_verifiable.includes(:definition, recipient: :user).find_by!(public_id: params[:id])
end
end
end
2 changes: 2 additions & 0 deletions app/models/admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class Admin < ResourceRecord
# add has_one associations above.

# add has_many associations above.
has_many :created_credential_awards, class_name: "Credentials::Award", foreign_key: :created_by_admin_id, dependent: :restrict_with_error
has_many :credential_events, class_name: "Credentials::Event", dependent: :nullify

# add attachments above.

Expand Down
15 changes: 15 additions & 0 deletions app/views/site/credentials/badge.svg.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-labelledby="title desc">
<title id="title"><%= @award.definition.badge_alt_text %></title>
<desc id="desc">A DevCongress credential badge for <%= @award.definition.name %>.</desc>
<% dark = @theme == "dark" %>
<rect width="1024" height="1024" rx="128" fill="<%= dark ? "#111111" : "#f5e642" %>"/>
<rect x="38" y="38" width="948" height="948" rx="102" fill="none" stroke="<%= dark ? "#f5e642" : "#111111" %>" stroke-width="28"/>
<circle cx="512" cy="330" r="182" fill="#e8117f" stroke="<%= dark ? "#f5e642" : "#111111" %>" stroke-width="24"/>
<text x="512" y="390" text-anchor="middle" font-family="Georgia, serif" font-size="176" font-weight="700" fill="#ffffff">d:c</text>
<text x="512" y="606" text-anchor="middle" font-family="Arial, sans-serif" font-size="58" font-weight="800" letter-spacing="8" fill="<%= dark ? "#ffffff" : "#111111" %>">DEVCONGRESS</text>
<line x1="190" y1="650" x2="834" y2="650" stroke="<%= dark ? "#f5e642" : "#111111" %>" stroke-width="12"/>
<text x="512" y="742" text-anchor="middle" font-family="Arial, sans-serif" font-size="54" font-weight="800" fill="<%= dark ? "#f5e642" : "#111111" %>">COMMUNITY</text>
<text x="512" y="812" text-anchor="middle" font-family="Arial, sans-serif" font-size="54" font-weight="800" fill="<%= dark ? "#f5e642" : "#111111" %>">CONTRIBUTOR</text>
<text x="512" y="918" text-anchor="middle" font-family="monospace" font-size="28" fill="<%= dark ? "#ffffff" : "#333333" %>"><%= @award.public_id %></text>
</svg>
24 changes: 24 additions & 0 deletions app/views/site/credentials/certificate.svg.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1120" viewBox="0 0 1600 1120" role="img" aria-labelledby="title desc">
<title id="title"><%= @award.definition.name %> certificate for <%= @award.recipient_name %></title>
<desc id="desc">A printable DevCongress certificate with recipient, achievement, issue date, and verification identifier.</desc>
<rect width="1600" height="1120" fill="#f5f2e8"/>
<rect x="42" y="42" width="1516" height="1036" rx="28" fill="#ffffff" stroke="#111111" stroke-width="8"/>
<rect x="42" y="42" width="1516" height="118" rx="28" fill="#f5e642"/>
<rect x="42" y="132" width="1516" height="28" fill="#f5e642"/>
<text x="110" y="116" font-family="Georgia, serif" font-size="58" font-weight="700"><tspan fill="#e8117f">dev</tspan><tspan fill="#111111">:</tspan><tspan fill="#555555">congress{};</tspan></text>
<text x="1490" y="112" text-anchor="end" font-family="Arial, sans-serif" font-size="28" font-weight="700" letter-spacing="5">VERIFIED CREDENTIAL</text>
<text x="800" y="285" text-anchor="middle" font-family="Arial, sans-serif" font-size="26" font-weight="700" letter-spacing="7" fill="#777777">DEVCONGRESS RECOGNIZES</text>
<text x="800" y="405" text-anchor="middle" font-family="Georgia, serif" font-size="82" fill="#111111"><%= @award.recipient_name %></text>
<line x1="310" y1="450" x2="1290" y2="450" stroke="#e8117f" stroke-width="6"/>
<text x="800" y="540" text-anchor="middle" font-family="Arial, sans-serif" font-size="32" fill="#555555">for earning the</text>
<text x="800" y="642" text-anchor="middle" font-family="Georgia, serif" font-size="70" fill="#111111"><%= @award.definition.name %></text>
<foreignObject x="270" y="695" width="1060" height="130">
<div xmlns="http://www.w3.org/1999/xhtml" style="font: 28px Arial, sans-serif; line-height: 1.45; text-align: center; color: #555555;"><%= @award.definition.short_description %></div>
</foreignObject>
<text x="240" y="940" font-family="Arial, sans-serif" font-size="22" font-weight="700" fill="#777777">ISSUED</text>
<text x="240" y="982" font-family="Arial, sans-serif" font-size="30" fill="#111111"><%= @award.issued_at.to_date.to_fs(:long) %></text>
<text x="1360" y="940" text-anchor="end" font-family="Arial, sans-serif" font-size="22" font-weight="700" fill="#777777">CREDENTIAL ID</text>
<text x="1360" y="982" text-anchor="end" font-family="monospace" font-size="28" fill="#111111"><%= @award.public_id %></text>
<text x="800" y="1040" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" fill="#777777">Verify at <%= public_credential_url(@award.public_id) %></text>
</svg>
108 changes: 108 additions & 0 deletions app/views/site/credentials/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<% award = @award %>
<% definition = award.definition %>
<% verification_url = public_credential_url(award.public_id) %>
<% content_for :title, "#{definition.name} · #{award.recipient_name}" %>
<% content_for :description, "Verify #{award.recipient_name}'s #{definition.name}, issued by DevCongress." %>
<% structured_data({
"@context" => "https://schema.org",
"@type" => "EducationalOccupationalCredential",
"name" => definition.name,
"description" => definition.description,
"credentialCategory" => "Certificate",
"recognizedBy" => {"@type" => "Organization", "name" => "DevCongress", "url" => "https://devcongress.org/"},
"about" => {"@type" => "Person", "name" => award.recipient_name},
"dateCreated" => award.issued_at&.to_date&.iso8601,
"expires" => award.expires_at&.to_date&.iso8601,
"identifier" => award.public_id,
"url" => verification_url
}.compact) %>

<div class="dc-container max-w-5xl py-10 sm:py-16">
<header class="grid gap-8 rounded-2xl border-2 border-[#111] bg-white p-6 shadow-[8px_8px_0_#e8117f] md:grid-cols-[220px_1fr] md:p-10">
<div>
<%= image_tag credential_badge_path(award.public_id, format: :svg), alt: definition.badge_alt_text,
class: "aspect-square w-full max-w-[220px] rounded-2xl border-2 border-[#111]" %>
</div>
<div>
<div class="flex flex-wrap items-center gap-3">
<span class="dc-eyebrow !mb-0">Verified DevCongress credential</span>
<% status_classes = {"active" => "bg-emerald-100 text-emerald-800", "expired" => "bg-amber-100 text-amber-900", "revoked" => "bg-red-100 text-red-800"} %>
<span class="rounded-full px-3 py-1 text-xs font-bold uppercase tracking-wide <%= status_classes.fetch(award.status) %>"><%= award.status %></span>
</div>
<h1 class="dc-heading mt-3 text-4xl sm:text-5xl"><%= definition.name %></h1>
<p class="mt-3 text-xl font-semibold text-[#333]">Awarded to <%= award.recipient_name %></p>
<p class="mt-4 max-w-2xl text-[#555]"><%= definition.short_description %></p>

<dl class="mt-6 grid gap-4 text-sm sm:grid-cols-2">
<div><dt class="font-semibold uppercase tracking-wide text-[#777]">Issued by</dt><dd class="mt-1">DevCongress</dd></div>
<div><dt class="font-semibold uppercase tracking-wide text-[#777]">Issue date</dt><dd class="mt-1"><%= award.issued_at.to_date.to_fs(:long) %></dd></div>
<% if award.expires_at %><div><dt class="font-semibold uppercase tracking-wide text-[#777]">Expires</dt><dd class="mt-1"><%= award.expires_at.to_date.to_fs(:long) %></dd></div><% end %>
<div><dt class="font-semibold uppercase tracking-wide text-[#777]">Credential ID</dt><dd class="mt-1 font-mono"><%= award.public_id %></dd></div>
</dl>
</div>
</header>

<% unless award.active? %>
<section class="mt-10 rounded-xl border-2 <%= award.revoked? ? "border-red-700 bg-red-50" : "border-amber-700 bg-amber-50" %> p-6" role="status">
<h2 class="text-xl font-bold">This credential is <%= award.status %>.</h2>
<% if award.revoked? %><p class="mt-2"><%= award.revoked_reason %></p><% end %>
<p class="mt-2 text-sm">This verification URL remains available so a previously shared credential never becomes ambiguous.</p>
</section>
<% end %>

<div class="mt-12 grid gap-10 lg:grid-cols-[1.2fr_0.8fr]">
<div class="space-y-10">
<section>
<h2 class="dc-heading text-3xl">What this credential recognizes</h2>
<p class="mt-4 text-[#555]"><%= definition.description %></p>
</section>

<section>
<h2 class="dc-heading text-3xl">Criteria and evidence</h2>
<h3 class="mt-4 font-semibold">Eligibility criteria</h3>
<div class="prose mt-2 max-w-none prose-a:text-[#e8117f]"><%= simple_format(definition.eligibility_criteria) %></div>
<h3 class="mt-5 font-semibold">Evidence reviewed</h3>
<p class="mt-2 text-[#555]"><%= award.evidence_summary %></p>
<p class="mt-3 text-sm text-[#777]">Assessed against criteria version <%= award.criteria_version %>.</p>
</section>

<section>
<h2 class="dc-heading text-3xl">What it does not prove</h2>
<p class="mt-4 text-[#555]"><%= definition.does_not_prove %></p>
</section>
</div>

<aside class="space-y-6">
<% if award.active? %>
<section class="rounded-xl border-2 border-[#111] bg-[#fefce8] p-6">
<h2 class="text-xl font-bold">Add to LinkedIn</h2>
<p class="mt-2 text-sm text-[#555]">LinkedIn does not prefill certification details. Copy each value below, then open LinkedIn's official form. For the issuer, select <%= link_to "DevCongress's LinkedIn Page", "https://www.linkedin.com/company/28183790/", target: "_blank", rel: "noopener noreferrer", class: "font-semibold text-[#e8117f] underline" %> from LinkedIn's suggestions so the organization logo is attached.</p>
<dl class="mt-5 space-y-4">
<% award.linkedin_fields(verification_url:).each do |label, value| %>
<div data-controller="clipboard" data-clipboard-text-value="<%= value %>">
<dt class="text-xs font-bold uppercase tracking-wide text-[#777]"><%= label %></dt>
<dd class="mt-1 flex items-start justify-between gap-3 rounded-lg border border-[#d6d2c6] bg-white px-3 py-2 text-sm">
<span class="break-all"><%= value %></span>
<button type="button" data-action="clipboard#copy" class="shrink-0 font-semibold text-[#e8117f]"><span data-clipboard-target="label">Copy</span></button>
</dd>
</div>
<% end %>
</dl>
<%= link_to "Add to LinkedIn", Credentials::Award::LINKEDIN_ADD_TO_PROFILE_URL,
target: "_blank", rel: "noopener noreferrer", class: "dc-btn dc-btn-primary mt-6 w-full" %>
<p class="mt-4 text-xs text-[#666]">This adds a license or certification. It does not create a LinkedIn verification badge or imply LinkedIn endorsement.</p>
</section>
<% end %>

<section class="rounded-xl border-2 border-[#111] bg-white p-6">
<h2 class="text-xl font-bold">Download and share</h2>
<div class="mt-4 grid gap-3">
<%= link_to "Download certificate", credential_certificate_path(award.public_id, format: :svg), class: "dc-btn dc-btn-yellow w-full" %>
<%= link_to "Download light badge", credential_badge_path(award.public_id, format: :svg), download: "#{definition.slug}-badge.svg", class: "dc-btn dc-btn-white w-full" %>
<%= link_to "Download dark badge", credential_badge_path(award.public_id, format: :svg, theme: "dark"), download: "#{definition.slug}-badge-dark.svg", class: "dc-btn dc-btn-outline w-full" %>
</div>
<p class="mt-4 text-xs text-[#666]">Use the badge only with this verification URL. It represents the achievement above—not employment, a degree, or LinkedIn verification.</p>
</section>
</aside>
</div>
</div>
6 changes: 6 additions & 0 deletions config/recurring.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ production:
queue: default
schedule: every hour at minute 20
description: "Close #jobs Slack messages for jobs that have expired"

credential_expiry:
class: Credentials::ExpiryJob
queue: default
schedule: every hour at minute 35
description: "Mark credentials past their configured expiry date as expired"
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
get "companies/:slug", to: "companies#show", as: :public_company
get "projects", to: "projects#index", as: :public_projects
get "projects/:slug", to: "projects#show", as: :public_project
get "credentials/:id", to: "credentials#show", as: :public_credential,
constraints: {id: /[1-9A-HJ-NP-Za-km-z]{24}/}
get "credentials/:id/badge", to: "credentials#badge", as: :credential_badge,
defaults: {format: :svg}, constraints: {id: /[1-9A-HJ-NP-Za-km-z]{24}/}
get "credentials/:id/certificate", to: "credentials#certificate", as: :credential_certificate,
defaults: {format: :svg}, constraints: {id: /[1-9A-HJ-NP-Za-km-z]{24}/}

# Unsubscribe links in emails (no sign-in; the signed token names the user).
get "unsubscribe/:token", to: "unsubscribes#show", as: :unsubscribe, format: false
Expand Down
74 changes: 73 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading