Skip to content
Open
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
10 changes: 6 additions & 4 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ course_email: COURSE_EMAIL@berkeley.edu
gradescope_course_id: 123456 # you can find this in the Gradescope URL after /courses
bcourses_course_id: 123456 # Same as above, but for bCourses. Leave blank if not in use...
ed_course_id: 123456 # Again, same as above.
sememster: spYY | suYY | faYY # set for the current seemester
semester: spYY | suYY | faYY # set for the current semester
semester_start_date: '2025-01-21' # Required for the archive banner logic (YYYY-MM-DD)
semester_end_date: '2025-05-09' # Required for the archive banner logic (YYYY-MM-DD)
# TODO(setup): Set this to the file path of your course logo image, or leave it blank to display your course name.
# To edit the size and positioning of the logo/course name, see _sass/custom/course_overrides.scss
logo: /assets/images/berkeley_walking_bear.png
# This should be one of eecs, dsus, stat
# (Future) This will control some footer text, and later custom styling.
course_department: dsus
# This should be the page of all class archives
# Typically just / for DS courses (with a visible index page), or /archives if you're hosting your own, or a link to the inst.eecs page
# If you have no archive page, comment this line out or leave blank.
# This path/URL is used by the archive banner shown when the site date is outside the semester window.
# Keep this key present. Default `/` is fine for class-listing homepages, and `/archive` is also a good option.
# We recommend listing archived offerings as "Archive" or "Not Updated" in course listings.
class_archive_path: /

# TODO(setup): Remove this line if your favicon is named favicon.ico and is in the root directory,
Expand Down
41 changes: 41 additions & 0 deletions _includes/header_custom.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% assign archive_url = site.class_archive_path | absolute_url %}
<div id="archive-banner" class="archive-banner" role="status">
This site is archived. Please check:
<a id="archive-banner-link" class="archive-banner-link" href="{{ archive_url }}">{{ archive_url }}</a>
</div>

<script>
(() => {
const semesterStart = {{ site.semester_start_date | jsonify }};
const semesterEnd = {{ site.semester_end_date | jsonify }};
if (!semesterStart || !semesterEnd) {
return;
}

const parseDate = (value) => {
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
};

const startDate = parseDate(semesterStart);
const endDate = parseDate(semesterEnd);
if (!startDate || !endDate) {
return;
}

endDate.setHours(23, 59, 59, 999);
const currentDate = new Date();
const isOutsideSemester = currentDate < startDate || currentDate > endDate;
if (!isOutsideSemester) {
return;
}

const banner = document.getElementById('archive-banner');
if (!banner) {
return;
}

banner.style.display = 'block';
document.body.classList.add('archive-banner-visible');
})();
</script>
46 changes: 44 additions & 2 deletions _plugins/config_validator.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# frozen_string_literal: true

require 'date'
require 'uri'

# A UC Berkeley-specific validator for Jekyll sites
# This class validates the following options:
# 1) Ensures required attributes are present in the config file.
Expand Down Expand Up @@ -44,7 +47,10 @@ class ConfigValidator
url: :validate_clean_url,
baseurl: :validate_semester_format,
course_department: :inclusion_validator,
color_scheme: :inclusion_validator
color_scheme: :inclusion_validator,
semester_start_date: :validate_iso8601_date,
semester_end_date: :validate_iso8601_date,
class_archive_path: :validate_archive_path
}.freeze

attr_accessor :config, :errors
Expand All @@ -61,13 +67,15 @@ def validate
send(validator, key, config[key.to_s]) if @config.key?(key.to_s)
end

validate_semester_date_range

raise ConfigValidationError, errors if errors.length.positive?

puts 'Passed Berkeley YAML Config Validations'
end

def validate_keys!
required_keys = %i[baseurl course_department]
required_keys = %i[baseurl course_department semester_start_date semester_end_date class_archive_path]
required_keys.each do |key|
errors << "#{key} is missing from site config" unless @config.key?(key.to_s)
end
Expand Down Expand Up @@ -95,6 +103,40 @@ def inclusion_validator(key, value)
allowed = self.class.const_get("VALID_#{key.upcase}")
errors << "`#{key}` must be one of #{allowed} (not '#{value}')" unless allowed.include?(value)
end

def validate_iso8601_date(key, value)
return if parse_iso8601_date(value)

errors << "`#{key}` must be a valid ISO-8601 date string (YYYY-MM-DD), not '#{value}'"
end

def validate_semester_date_range
start_date = parse_iso8601_date(config['semester_start_date'])
end_date = parse_iso8601_date(config['semester_end_date'])
return unless start_date && end_date
return unless start_date > end_date

errors << '`semester_start_date` must be on or before `semester_end_date`'
end

def validate_archive_path(_key, value)
path = value.to_s.strip
return errors << '`class_archive_path` cannot be blank' if path.empty?
return if path.match?(%r{^/})

uri = URI.parse(path)
return if uri.is_a?(URI::HTTP) && uri.host

errors << "`class_archive_path` must be an absolute path starting with `/` or a full URL, not '#{value}'"
rescue URI::InvalidURIError
errors << "`class_archive_path` must be an absolute path starting with `/` or a full URL, not '#{value}'"
end

def parse_iso8601_date(value)
Date.iso8601(value.to_s)
rescue Date::Error
nil
end
end
end

Expand Down
1 change: 1 addition & 0 deletions _sass/berkeley/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ $serif-font-family: 'Source Serif 4', 'Georiga', 'Baskerville', serif;

$berkeley-blue-old: #003262;
$berkeley-blue: #002676; // rgb(0, 38, 118);
$berkeley-rose-medium: #d02670;

// Colors
$link-color: $berkeley-blue;
Expand Down
31 changes: 28 additions & 3 deletions _sass/custom/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,34 @@ p.note::before {
@include btn-color($white, #6929c4);
}

.btn-officehours{
@include btn-color($white, #009d9a);
}
.btn-officehours{
@include btn-color($white, #009d9a);
}

.archive-banner {
display: none;
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 999;
padding: 0.75rem 1rem;
background-color: $berkeley-rose-medium;
color: #fff;
font-weight: 700;
text-align: center;
}

.archive-banner-link,
.archive-banner-link:visited {
color: #fff;
font-weight: 700;
text-decoration: underline;
}

body.archive-banner-visible {
padding-top: 3rem;
}

@if $color-scheme == dark {
@include dark-mode-overrides;
Expand Down
63 changes: 63 additions & 0 deletions spec/jekyll/config_validator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

require 'spec_helper'
require_relative '../../_plugins/config_validator'

RSpec.describe Jekyll::ConfigValidator do
let(:base_config) do
{
'baseurl' => '/sp26',
'course_department' => 'dsus',
'semester_start_date' => '2026-01-21',
'semester_end_date' => '2026-05-09',
'class_archive_path' => '/'
}
end

it 'validates with required semester dates present' do
validator = described_class.new(base_config)

expect { validator.validate }.not_to raise_error
end

it 'requires semester_start_date and semester_end_date' do
config = base_config.except('semester_start_date', 'semester_end_date')
validator = described_class.new(config)

expect { validator.validate }
.to raise_error(ConfigValidationError, /semester_start_date is missing.*semester_end_date is missing/m)
end

it 'requires class_archive_path' do
validator = described_class.new(base_config.except('class_archive_path'))

expect { validator.validate }
.to raise_error(ConfigValidationError, /class_archive_path is missing from site config/)
end

it 'allows class_archive_path as a full URL' do
validator = described_class.new(base_config.merge('class_archive_path' => 'https://c88c.org/archive'))

expect { validator.validate }.not_to raise_error
end

it 'rejects class_archive_path values that are not paths or full URLs' do
validator = described_class.new(base_config.merge('class_archive_path' => 'archive'))
expected = %r{`class_archive_path` must be an absolute path starting with `/` or a full URL}
expect { validator.validate }.to raise_error(ConfigValidationError, expected)
end

it 'validates ISO-8601 semester dates' do
validator = described_class.new(base_config.merge('semester_start_date' => 'spring-2026'))

expect { validator.validate }
.to raise_error(ConfigValidationError, /`semester_start_date` must be a valid ISO-8601 date string/)
end

it 'requires the start date to be on or before the end date' do
validator = described_class.new(base_config.merge('semester_start_date' => '2026-06-01'))

expect { validator.validate }
.to raise_error(ConfigValidationError, /`semester_start_date` must be on or before `semester_end_date`/)
end
end