diff --git a/.github/.scout-dictionary b/.github/.scout-dictionary index df7f242..818cf35 100644 --- a/.github/.scout-dictionary +++ b/.github/.scout-dictionary @@ -1,15 +1,22 @@ boonie brookville -Campmor +campmor clifty -Crocs +crocs +dateutil +env epi +exif fiorella firem'n +idx +img iols jaccos +kebabed koczan krietenstein +lanczos leppert lubbe maumee @@ -17,7 +24,9 @@ mmr neese oa ons +pil sharin +slugified stoddard totin' towne diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f4b5b90..4d1cb7a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,17 @@ -* @shoverbj +# ============================================================================== +# GitHub Code Owners Configuration File +# This file automatically assigns reviewers to Pull Requests based on the paths +# of the files being modified. +# +# Rules to remember: +# 1. Order matters: Later rules override earlier ones for the same file paths. +# 2. Users can be specified by @username or by an email address linked to their account. +# 3. Teams can be assigned using @org/team-name (requires GitHub Organization). +# ============================================================================== + +# ------------------------------------------------------------------------------ +# GLOBAL FALLBACK OWNERS +# ------------------------------------------------------------------------------ +# The asterisk (*) matches every file in the repository. +# These users will be requested to review a PR if no more specific rule below matches. +* @shoverbj diff --git a/.github/ISSUE_TEMPLATE/01-new-blog-post.yml b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml new file mode 100644 index 0000000..ede626d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-new-blog-post.yml @@ -0,0 +1,174 @@ +# ============================================================================== +# GitHub Issue Form Template Configuration +# This file defines a structured form for submitting new blog posts. +# ============================================================================== + +# Core metadata for the GitHub Issue Form template +name: New Blog Post # Name of the template displayed in the GitHub UI +description: "Add a new post to the blog" # Subtext explaining the purpose of this form +labels: ["blog"] # Automatically applies this label to the created issue +title: "[New Blog Post]" # Pre-fills the issue title with this prefix +body: # Begins the list of form UI components/fields + + # ---------------------------------------------------------------------------- + # FIELD 1: Introductory Instructions (Markdown Notice) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Literal block scalar to preserve line breaks + **The title above is just for internal use. Please don't change it** + + # Creating new blog post + + Fill out this form and click "Create" below to submit a new blog post. + After submitting your post, it will be reviewed by the webmaster before + being posted to the website. + + # ---------------------------------------------------------------------------- + # FIELD 2: Blog Post Title Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: title # Unique identifier used by automated scripts + attributes: # Map of properties for this text input + label: "Blog Title" # Heading text displayed above the text box + description: | # Subtext providing user instructions + Title for your post + + This should include the campout/adventure/event if thats what this post + is about! + placeholder: "πŸ‘‰ REPLACE WITH YOUR TITLE πŸ‘ˆ" # Temporary ghost text inside the empty box + validations: # Section for field constraints + required: true # User must fill this out to submit the form + + # ---------------------------------------------------------------------------- + # FIELD 3: Publication Date Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: date # Unique identifier for the date property + attributes: # Map of properties for this text input + label: "Post Date" # Heading text displayed above the text box + description: | # Instruction subtext + Date for your blog post + + If this is left empty, today's date will be used. + placeholder: YYYY-MM-DD # Example format text shown inside the empty field + validations: # Section for field constraints + required: false # Makes this an optional field for the user + + # ---------------------------------------------------------------------------- + # FIELD 4: Author Selection Dropdown + # ---------------------------------------------------------------------------- + - type: dropdown # Selectable list component + id: authors # Unique identifier for the authors list + attributes: # Map of properties for this dropdown + label: "Authors" # Heading text displayed above the dropdown + description: "Select author(s) of this post" # Instruction subtext + multiple: true # Allows users to choose more than one option + options: # List of selectable choices + # NOTE: Do not change the line below. It is used by our GitHub Action. + # AUTHOR_START + - Chris Koczan + - Benjamin Shover + # AUTHOR_END + validations: # Section for field constraints + required: true # User must choose at least one author + + # ---------------------------------------------------------------------------- + # FIELD 5: Missing Author Instructions (Markdown Link) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Text layout block with a link to add new authors + If your name is not in the list of authors, please submit a request to + add your name to the list of authors here: + + [New Author Request](https://github.com/scouting331/scoutSite/issues/new?template=03-new-blog-author.yml) + + # ---------------------------------------------------------------------------- + # FIELD 6: Unit / Tag Dropdown Selection + # ---------------------------------------------------------------------------- + - type: dropdown # Selectable list component + id: tags # Unique identifier (maps to post categories or tags) + attributes: # Map of properties for this dropdown + label: "Unit" # Heading text displayed above the dropdown + description: | # Instruction subtext + Select the unit(s) to associate this post with. + + Multiple units may be selected. + multiple: true # Allows selection of multiple scouting units + options: # List of available scouting units + - Troop 303 + - Troop 331 + - Crew 303 + - Pack 303 + validations: # Section for field constraints + required: true # User must select at least one unit + + # ---------------------------------------------------------------------------- + # FIELD 7: Cover Photo File Attachment + # ---------------------------------------------------------------------------- + - type: upload # File attachment component + id: cover-photo # Unique identifier for the cover photo asset + attributes: # Map of properties for this upload field + label: "Cover Photo" # Heading text displayed above the dropzone + description: | # Subtext containing processing warnings + Photo shown at top of your blog post and on the homepage card + + **NOTE**: There is no way to limit the number of files in this space. Please + only upload 1 photo. If multiple photos are uploaded, the script will + simply choose one photo and discard the rest. + validations: # Section for file upload constraints + required: false # Cover photo is not mandatory to submit + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" # Allowed image extensions + + # ---------------------------------------------------------------------------- + # FIELD 8: Photo Album File Attachment List + # ---------------------------------------------------------------------------- + - type: upload # File attachment component + id: photo-album # Unique identifier for the gallery asset batch + attributes: # Map of properties for this upload field + label: "Photo Album" # Heading text displayed above the dropzone + description: | # Subtext containing file size limits + If you want to include a photo album upload all of the photos you want + included here. + + **NOTE**: If the file is too big (>25 Mb), the photo will need to be resized + before being uploaded. + validations: # Section for file upload constraints + required: false # Photo gallery is not mandatory to submit + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" # Allowed image extensions + + # ---------------------------------------------------------------------------- + # FIELD 9: Main Body Content Textarea + # ---------------------------------------------------------------------------- + - type: textarea # Large multi-line text input block + id: blog-content # Unique identifier for the markdown body text + attributes: # Map of properties for this textarea + label: "Blog Text" # Heading text displayed above the textarea + description: | # Instruction subtext + Write your blog post here! + placeholder: | # Boilerplate template text pre-filled inside the box + + + ## What We Did + + - Activity 1 + - Activity 2 + - Activity 3 + + :::infoβ›Ί Outdoor Adventure Tip + + If this post is about a camping trip, use this box to share a tip about + the gear or recipes we used! + + ::: + validations: # Section for field constraints + required: true # User must enter text content to submit + + # ---------------------------------------------------------------------------- + # FIELD 10: Closing Instructions (Markdown Footer) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Final warning text for form submitters + **Please don't edit any of the other options!** \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/02-new-document.yml b/.github/ISSUE_TEMPLATE/02-new-document.yml new file mode 100644 index 0000000..5574f4e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-new-document.yml @@ -0,0 +1,96 @@ +# ============================================================================== +# GitHub Issue Form Template Configuration +# This file defines a structured form for submitting new site documentation. +# ============================================================================== + +# Core metadata for the GitHub Issue Form template +name: New Document # Name of the template displayed in the GitHub UI +description: "Add a new document" # Subtext explaining the purpose of this form +labels: ["docs"] # Automatically applies this label to the created issue +title: "[New Document]" # Pre-fills the issue title with this prefix +body: # Begins the list of form UI components/fields + + # ---------------------------------------------------------------------------- + # FIELD 1: Introductory Instructions (Markdown Notice) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Literal block scalar to preserve line breaks + **The title above is just for internal use. Please don't change it** + + # Adding a new document to the site + + Fill out this form and click "Create" below to submit a new document. + After submitting your document, it will be reviewed by the webmaster before + being added to the website. + + # ---------------------------------------------------------------------------- + # FIELD 2: Document Title Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: title # Unique identifier used by automated scripts + attributes: # Map of properties for this text input + label: "Document Title" # Heading text displayed above the text box + description: | # Subtext providing user instructions + Title of the proposed document + + This will be displayed at the top of the document. Make sure it is descriptive of what the document is! + placeholder: "New Document" # Temporary ghost text inside the empty box + validations: # Section for field constraints + required: true # User must fill this out to submit the form + + # ---------------------------------------------------------------------------- + # FIELD 3: Document Description Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: description # Unique identifier for the description field + attributes: # Map of properties for this text input + label: "Document Description" # Heading text displayed above the text box + description: | # Instruction subtext + Please provide a single sentence description of what this document is. + placeholder: "This is the packing list/rules/etc..." # Ghost text showing an example description + validations: # Section for field constraints + required: true # User must provide a description to submit + + # ---------------------------------------------------------------------------- + # FIELD 4: Unit Selection Dropdown + # ---------------------------------------------------------------------------- + - type: dropdown # Selectable list component + id: tags # Unique identifier for the associated unit/tags + attributes: # Map of properties for this dropdown + label: "Unit" # Heading text displayed above the dropdown + description: | # Instruction subtext explaining general option + Select the unit(s) to associate this post with. + + If this document applies to multiple units, select **General** + multiple: false # Restricts the user to choosing exactly one option + options: # List of available scouting units and categories + - Troop 303 + - Troop 331 + - Crew 303 + - Pack 303 + - General # (Fallback for cross-unit files) + validations: # Section for field constraints + required: true # User must choose a unit to submit the form + + # ---------------------------------------------------------------------------- + # FIELD 5: Main Body Content Textarea + # ---------------------------------------------------------------------------- + - type: textarea # Large multi-line text input block + id: doc-content # Unique identifier for the document content + attributes: # Map of properties for this textarea + label: "Document Text" # Heading text displayed above the textarea + description: | # Subtext noting that files/images can be attached + Add the content of your document here. You can include inline + images/files if needed. + placeholder: "Document text here..." # Default ghost text inside the text editor box + validations: # Section for field constraints + required: true # User must enter text content to submit + + # ---------------------------------------------------------------------------- + # FIELD 6: Closing Instructions (Markdown Footer) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Final warning text for form submitters + **Please don't edit any of the other options!** diff --git a/.github/ISSUE_TEMPLATE/03-new-blog-author.yml b/.github/ISSUE_TEMPLATE/03-new-blog-author.yml new file mode 100644 index 0000000..0666193 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-new-blog-author.yml @@ -0,0 +1,70 @@ +# ============================================================================== +# GitHub Issue Form Template Configuration +# This file defines the structured submission form used by the onboarding script +# to extract new author metadata, roles, and profile avatars. +# ============================================================================== + +# Core metadata for the GitHub Issue Form template +name: New Blog Author # Name of the template displayed in the GitHub UI +description: "Add a new author to the blog authors list" # Subtext explaining the purpose of this form +title: "[New Author]" # Pre-fills the issue title with this prefix +labels: ["new-author"] # Applies the label used by the automation script to trigger runs +body: # Begins the list of form UI components/fields + + # ---------------------------------------------------------------------------- + # FIELD 1: Introductory Instructions (Markdown Notice) + # ---------------------------------------------------------------------------- + - type: markdown # Static text block using Markdown formatting + attributes: # Map of properties for this markdown block + value: | # Literal block scalar to preserve line breaks + **The title above is just for internal use. Please don't change it** + + # ---------------------------------------------------------------------------- + # FIELD 2: Author's Name Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: name # Unique identifier read by data.get("name") in the Python script + attributes: # Map of properties for this text input + label: Author's Name # Heading text displayed above the text box + description: | # Subtext outlining important privacy rules for youth + Name shown in blog posts for the author + + **NOTE**: If you are a Scout this should be your first name followed by + a last initial, **NO LAST NAMES PLEASE** + + If you are an adult and want your last name shown, feel free to include it" + placeholder: "Scout A" # Temporary ghost text demonstrating correct formatting + validations: # Section for field constraints + required: true # Script will exit with code 1 if this field is missing + + # ---------------------------------------------------------------------------- + # FIELD 3: Author Title / Role Input + # ---------------------------------------------------------------------------- + - type: input # Single-line text input field + id: title # Unique identifier read by data.get("title") in the Python script + attributes: # Map of properties for this text input + label: Title # Heading text displayed above the text box + description: "List your position(s) and unit." # Instructions for submission + placeholder: "Position, Troop ###" # Visual example of expected input format + validations: # Section for field constraints + required: true # Script will exit with code 1 if this field is missing + + # ---------------------------------------------------------------------------- + # FIELD 4: Avatar Image File Attachment + # ---------------------------------------------------------------------------- + - type: upload # File attachment upload component + id: image_url # Unique identifier read by data.get("image_url") in the Python script + attributes: # Map of properties for this upload field + label: Avatar Image # Heading text displayed above the dropzone + description: | # Instructions containing sizing metrics and processing alerts + This is the photo that will be associated with you. Please make sure the + image size is relatively small (500 x 500 pixels is ideal) and your face + is centered in the image. + + **NOTE**: There is no way to limit the number of files in this space. Please + only upload 1 photo. If multiple photos are uploaded, the script will + simply choose one photo and discard the rest. + validations: # Section for file upload constraints + required: false # Script allows missing image files and skips processing if empty + accept: ".png, .jpg, .jpeg, .gif, .svg, .webp" # Supported image extension types for conversion + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 8457aa9..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "" -labels: "" -assignees: "" ---- - -**Describe the bug** A clear and concise description of what the bug is. - -**To Reproduce** Steps to reproduce the behavior: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** A clear and concise description of what you expected to -happen. - -**Screenshots** If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - -- OS: [e.g. iOS] -- Browser [e.g. chrome, safari] -- Version [e.g. 22] - -**Smartphone (please complete the following information):** - -- Device: [e.g. iPhone6] -- OS: [e.g. iOS8.1] -- Browser [e.g. stock browser, safari] -- Version [e.g. 22] - -**Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/new-blog-post.md b/.github/ISSUE_TEMPLATE/new-blog-post.md deleted file mode 100644 index fd05fa5..0000000 --- a/.github/ISSUE_TEMPLATE/new-blog-post.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: New Blog Post -about: Add a new post to the blog -title: "" -labels: blog -assignees: shoverbj ---- diff --git a/.github/ISSUE_TEMPLATE/new-document.md b/.github/ISSUE_TEMPLATE/new-document.md deleted file mode 100644 index d46c59e..0000000 --- a/.github/ISSUE_TEMPLATE/new-document.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: New Document -about: Add a new document to the Unit Sites -title: "" -labels: documents -assignees: shoverbj ---- diff --git a/.github/scripts/add_author.py b/.github/scripts/add_author.py new file mode 100644 index 0000000..3143be9 --- /dev/null +++ b/.github/scripts/add_author.py @@ -0,0 +1,176 @@ +# .github/scripts/add_author.py +"""GitHub Action Automation Script for Author Onboarding. + +This script parses author profile details from a GitHub Issue payload, +validates the input fields, ensures author/slug uniqueness against an existing +YAML database, downloads and converts the author's avatar to WebP format, +and appends the new record to the project's central author configuration file. +It also updates the dropdown selections in the issue templates. + +Global Configurations: + AUTHORS_FILE (str): Path to the target author YAML registry database. + TEMPLATE_FILE (str): Path to the GitHub Issue form definition template. + AUTHORS_IMG_DIR (str): Destination directory for optimized author avatars. +""" + +import json +import yaml +import re +import os +import sys +import urllib.request +from PIL import Image, ImageOps + +# Constant definitions for project directories and structural files +AUTHORS_FILE = 'blog/authors.yml' +TEMPLATE_FILE = '.github/ISSUE_TEMPLATE/01-new-blog-post.yml' +AUTHORS_IMG_DIR = 'static/img/blog/authors' + +def main(): + """Process and onboard a new blog author from GitHub Actions issue data. + + This function extracts author metadata from an environment-supplied JSON + string, performs structural validation, generates a URL-safe unique slug, + downloads the remote avatar, converts it to WebP format, and appends the + finalized profile data to the project's authors registry document. It also + regenerates and sorts the author selection options inside the issue forms. + + Raises: + SystemExit (1): If required fields are missing, or if the author's + name already exists in the registry database. + """ + # Retrieve issue metadata passed as a JSON string from the GitHub Actions runner + issue_json = os.environ.get("ISSUE_DATA", "{}") + data = json.loads(issue_json) + + # Extract and normalize user string input data + author_name = data.get("name", "").strip() + author_title = data.get("title", "").strip() + raw_image_url = data.get("image_url", "").strip() + + # Isolate image URL using regex if it is wrapped inside Markdown syntax e.g., (https://url.com) + image_url = "" + url_match = re.search(r'\((https://[^\)]+)\)', raw_image_url) + if url_match: + image_url = url_match.group(1) + + # Terminate workflow execution if critical metadata is missing + if not author_name or not author_title: + print("Missing required fields. Exiting.") + sys.exit(1) + + # Read the existing document text to safely scan for duplicate profiles + raw_content = "" + if os.path.exists(AUTHORS_FILE): + with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: + raw_content = f.read() + + # Guard clause preventing duplicate submissions of existing author names + if f"name: {author_name}" in raw_content or f'name: "{author_name}"' in raw_content: + print(f"::error::The author name '{author_name}' already exists.") + sys.exit(1) + + # Initialize URL-safe slug creation by normalizing to lowercase alphanumeric characters + slug = author_name.lower() + slug = re.sub(r'[^a-z0-9\s-]', '', slug) + slug = re.sub(r'[\s-]+', '-', slug).strip('-') + + final_slug = slug + counter = 1 + + # Loop and append incremental numerical suffixes if slug collisions exist + while f"{final_slug}:" in raw_content: + final_slug = f"{slug}-{counter}" + counter += 1 + + # Process and optimize image resources if a valid link was detected + final_image_path = "" + if image_url: + os.makedirs(AUTHORS_IMG_DIR, exist_ok=True) + + # Discard tracking or token query strings from URL to isolate extension + clean_url = image_url.split("?") + _, ext = os.path.splitext(clean_url[0]) + if not ext: + ext = ".jpg" # Fall back to JPG extension if undetected + + # Stash download payload in server /tmp space + tmp_avatar_path = f"/tmp/raw_avatar{ext}" + try: + urllib.request.urlretrieve(image_url, tmp_avatar_path) + + # Setup image configuration names and location paths + target_file_name = f"{final_slug}.webp" + target_full_path = os.path.join(AUTHORS_IMG_DIR, target_file_name) + + # Execute image processing pipeline via Pillow (PIL) + with Image.open(tmp_avatar_path) as img: + img = ImageOps.exif_transpose(img) # Re-orient image according to metadata tags + if img.mode in ("P", "CMYK"): # Convert non-standard modes to preserve transparencies + img = img.convert("RGBA") + img.thumbnail((500, 500), Image.Resampling.LANCZOS) # High-fidelity scale reduction + img.save(target_full_path, format="WEBP", quality=85) # Save and optimize file space + + # Save the final relative public path to be referenced on the blog front-end + final_image_path = f"/img/blog/authors/{target_file_name}" + print(f"Successfully processed and saved avatar to {target_full_path}") + + # Clean up server system storage by removing the raw downloaded asset + if os.path.exists(tmp_avatar_path): + os.remove(tmp_avatar_path) + except Exception as e: + print(f"Warning: Failed to download or process avatar image. Error: {e}") + + # Build the textual YAML data mapping block to safely maintain standard docstrings + entry_lines = [ + f"{final_slug}:", + f" name: {author_name}", + f" title: {author_title}", + " page: true" + ] + if final_image_path: + entry_lines.append(f" image_url: {final_image_path}") + + raw_append_block = "\n" + "\n".join(entry_lines) + "\n" + + # Append structural string configurations to the bottom of the files document map + with open(AUTHORS_FILE, 'a', encoding='utf-8') as f: + f.write(raw_append_block) + print(f"Successfully appended new profile block to {AUTHORS_FILE}") + + # --- PART 2: REGEX SEARCH EXTRAC NAME ATTRIBUTES TO BUILD DROPDOWNS --- + with open(AUTHORS_FILE, 'r', encoding='utf-8') as f: + updated_raw_content = f.read() + + # Isolate individual text strings matching metadata properties + all_names = re.findall(r'^\s*name:\s*["\']?(.*?)["\']?\s*$', updated_raw_content, re.MULTILINE) + all_names = [n.strip() for n in all_names if n.strip()] + all_names.sort() # Arrange elements in alphanumeric order + + # Format items to valid list elements matching template indent spaces + yaml_lines = [f" - {name}" for name in all_names] + replacement_string = "\n".join(yaml_lines) + + # Perform regular expression lookup and insert the list inside the anchor tags + if os.path.exists(TEMPLATE_FILE): + with open(TEMPLATE_FILE, 'r') as f: + template_content = f.read() + + # Regex anchor block logic tracking target replacement parameters + pattern = r'(# AUTHOR_START\n)(.*?)(\n\s*# AUTHOR_END)' + updated_content = re.sub( + pattern, + f"\\1{replacement_string}\\3", + template_content, + flags=re.DOTALL + ) + + # Commit updated structured listings back to local document storage files + with open(TEMPLATE_FILE, 'w') as f: + f.write(updated_content) + print(f"Successfully updated dropdown in {TEMPLATE_FILE}") + else: + print(f"Warning: Template file {TEMPLATE_FILE} not found. Skipping dropdown injection.") + +if __name__ == "__main__": + main() diff --git a/.github/scripts/fb_router.py b/.github/scripts/fb_router.py new file mode 100644 index 0000000..7321c23 --- /dev/null +++ b/.github/scripts/fb_router.py @@ -0,0 +1,171 @@ +# .github/scripts/fb_router.py +"""Facebook Routing Automation for Docusaurus Blogs. + +This script parses recently added Docusaurus Markdown files, extracts their +metadata (title, slug, tags), and maps them against official scouting unit tag +taxonomies (`troop-303`, `troop-331`, `crew-303`, `pack-303`). It supports +multi-page routing by generating a compact JSON matrix array of all matched +target platforms, which is then streamed directly into the GitHub Actions runner. + +Global Configurations: + cmd (list): Set of base terminal commands used to poll the native Git tree history. + github_output_path (str): Pointer destination used to write runner outputs. +""" + +import os +import re +import subprocess +import sys +import json + +def get_last_commit_added_blog_file(): + """Finds the first newly added markdown file in the blog directory. + + Queries the local git repository history log for files added in the most + recent commit and filters them to guarantee they reside in the Docusaurus + 'blog/' subdirectory path and possess an authorized markdown extension. + + Returns: + str | None: The relative string file path of the discovered blog post, + or None if no matching files are found or an error occurs. + """ + try: + # Configures Git flag array: isolates newly added files ('A') in the last commit (HEAD~1 to HEAD) + cmd = ["git", "diff", "--name-only", "--diff-filter=A", "HEAD~1", "HEAD"] + + # Executes the Git process shell safely, capturing stdout text streams inside our runtime environment + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + files = result.stdout.splitlines() # Splits raw multi-line terminal block into clean array lists + + # Evaluates found assets, ensuring they live in the blog folder and terminate with valid markdown extensions + blog_files = [f for f in files if f.startswith("blog/") and f.endswith((".md", ".mdx"))] + return blog_files[0] if blog_files else None # Returns the earliest discovered item path string or None + except subprocess.CalledProcessError: + print("Error reading git diff.") # Log alert notifying developers to shell system errors + return None + +def parse_front_matter(file_path): + """Extracts title, slug, and tags from Docusaurus front matter. + + Reads the raw text of a markdown document using regular expressions to isolate + metadata properties. If a custom slug is omitted, it extracts the date components + (YYYY, MM, DD) and the trailing text title out of the file name to build a + standardized permalink array layout. + + Args: + file_path (str): The relative path targeting the markdown file. + + Returns: + tuple[str, str, str]: A tuple containing the extracted post title string, + the normalized relative path slug, and a cleaned block of tags text. + """ + # Safe open stream using standard global UTF-8 encoding rules to prevent character corruption + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + # Regex mapping logic hunting down front-matter metadata maps bounded between '---' markers + front_matter_match = re.search(r"^---\s*\n(.*?)\n---", content, re.DOTALL | re.MULTILINE) + if not front_matter_match: + return None, None, "" # Graceful exit fallback routing sequence if front-matter is missing + + fm_text = front_matter_match.group(1) # Isolates internal metadata configuration block + + # Extracts post title strings, matching text right of the identifier key prefix + title_match = re.search(r"^title:\s*(.*)\$", fm_text, re.MULTILINE) + title = title_match.group(1).strip(" '\"") if title_match else "New Blog Post" + + # Extracts permalink slug parameter, matching text right of the identifier key prefix + slug_match = re.search(r"^slug:\s*(.*)\$", fm_text, re.MULTILINE) + if slug_match: + slug = slug_match.group(1).strip(" '\"") + # Enforces a unified standard starting forward slash layout parameter if missing from custom slug + if not slug.startswith("/"): + slug = f"/{slug}" + else: + # Fallback to structural filename directory extraction if the slug key is omitted entirely + filename = os.path.basename(file_path) # Extracts the base filename context out of the path directory tree + base_name = os.path.splitext(filename)[0] # Separates base file text characters from trailing extensions + + # Regex to break "YYYY-MM-DD-filename" patterns into individual clean URL subdirectory fragments + date_match = re.match(r"^(\d{4})-(\d{2})-(\d{2})-(.*)\$", base_name) + if date_match: + year, month, day, clean_title = date_match.groups() + slug = f"/{year}/{month}/{day}/{clean_title}" # Stitches date tokens into permalink formats: /YYYY/MM/DD/title + else: + slug = f"/{base_name}" # Simple catch-all filename routing configuration format if dates are missing + + # Extracts raw tags line string parameter, converting entries to lower case for reliable string checking + tags_match = re.search(r"^tags:\s*(.*)\$", fm_text, re.MULTILINE) + tags_text = tags_match.group(1).lower() if tags_match else "" + + # Secondary array scanner block if tags are formatted across multi-line lists instead of single brackets + if not tags_text and "tags:" in fm_text: + tags_text = fm_text.split("tags:")[1].lower() # Isolates downstream array characters for processing + + return title, slug, tags_text + +def determine_target_pages(tags_text): + """Maps markdown tags to all matching scouting unit Facebook page categories. + + Scans the cleaned tag text string data blocks for occurrences of official + scouting unit key tokens (`troop-331`, `pack-303`, `crew-303`, `troop-303`). + Allows concurrent publishing configurations by mapping multiple items. + + Args: + tags_text (str): A string containing the post's front-matter tags. + + Returns: + list[str]: An array of uppercase matrix tokens (e.g., ["TROOP_303", "PACK_303"]) + specifying every intended social page destination channel. + ```""" + # Strips away formatting artifacts like quotes and brackets from raw array blocks + tags_clean = tags_text.replace("[", "").replace("]", "").replace('"', '').replace("'", "") + targets = [] # Allocation container list tracking targets + + # Independent conditional evaluation sequences enabling parallel multi-tag assignment mapping rules + if "troop-331" in tags_clean: + targets.append("TROOP_331") + if "pack-303" in tags_clean: + targets.append("PACK_303") + if "crew-303" in tags_clean: + targets.append("CREW_303") + if "troop-303" in tags_clean: + targets.append("TROOP_303") + + # Catch-all destination classification assignment bucket if no explicit unit tag is found + if not targets: + targets.append("DEFAULT") + + return targets + +def main(): + """Orchestrates script lifecycle execution. + + Coordinates the discovery, file text extraction, matrix categorization, and + exporting of blog post properties directly to the `GITHUB_OUTPUT` file stream + destination path for usage by downstream GitHub workspace workflow runners. + """ + new_file = get_last_commit_added_blog_file() # Polls git repository tree data for newest content logs + if not new_file: + print("No new blog files found.") + # Tells the main workflow that an article was not found, initializing safe shutdown variables + with open(os.environ["GITHUB_OUTPUT"], "a") as go: + go.write("has_new_post=false\n") + go.write("targets=[]\n") + sys.exit(0) # Regular clean operational termination sequence code + + # Executes data transformation pipelines against the discovered markdown asset + title, slug, tags_text = parse_front_matter(new_file) + targets = determine_target_pages(tags_text) + + # Expose metadata variables to subsequent GitHub Actions runner steps via the GITHUB_OUTPUT environment path + github_output_path = os.environ["GITHUB_OUTPUT"] # Locates dynamic engine runner variable tracking path link + with open(github_output_path, "a") as go: + go.write(f"has_new_post=true\n") # Tells the workflow step gate that data exists to process + go.write(f"title={title}\n") # Streams the post heading to populate status text bodies + go.write(f"slug={slug}\n") # Streams the relative link string array for absolute url rendering + # Dumps the Python array into a minified, stringified JSON array block string for GitHub Actions Matrix consumption + go.write(f"targets={json.dumps(targets)}\n") + +if __name__ == "__main__": + main() # Executes the application runtime lifecycle routine diff --git a/.github/scripts/generate_docs.py b/.github/scripts/generate_docs.py new file mode 100644 index 0000000..3559ffe --- /dev/null +++ b/.github/scripts/generate_docs.py @@ -0,0 +1,273 @@ +# .github/scripts/generate_docs.py +"""Docusaurus Document Generator and Asset Optimizer. + +This module automates the generation of MDX documents for Docusaurus from +structured GitHub Issue Form JSON data. It extracts front matter details +(title, description), downloads external asset attachments (inline body images +and files), and converts and compresses images into responsive WebP formats. + +Workflow Steps: + 1. Parses form data and normalizes string strings into URL keys (kebab-case). + 2. Traverses inline markdown text to collect, downscale, and swap out local assets. + 3. Downloads sequential photo attachments and places them inside an album asset directory. + 4. Compiles and exports a clean MDX file to the repository's native `docs/` folder. + +Environment Variables: + ISSUE_JSON (str): A stringified JSON object generated by `stefanbuck/github-issue-parser` + containing the raw form field mappings. + +Requirements: + - Pillow (PIL) + +File Structure Impact: + - Creates layout files at: `docs/{unit}/{slug}.mdx` + - Stores optimized assets at: `static/img/docs/{unit}/{slug}/` +""" +import os +import json +import re +import hashlib +import urllib.request +from io import BytesIO +from PIL import Image, ImageOps + +# --- Helper functions --- +def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): + """Optimizes, resizes, and converts images in an input directory to WebP format, + + saving them to an output directory while optionally renaming them using MD5 hashes. + + This function traverses an input folder structure, processes supported images + (.jpg, .jpeg, .png, .webp), applies EXIF orientation fixes, resizes them to + fit within a maximum bounding box, and saves the output. It purges the original + files from disk upon successful processing. It also links processed files back + to their original reference URLs if matching '.ref' sidecar files are present. + + Args: + input_dir (str): Path to the directory containing raw source images. + output_dir (str): Path to the destination directory where WebP images will be saved. + max_size (tuple of int, optional): Maximum (width, height) bounding box for resizing + using a LANCZOS filter. Defaults to (1920, 1080). + quality (int, optional): Compression quality parameter for WebP export, + ranging from 1 to 100. Defaults to 80. + keep_original_names (bool, optional): If True, retains the original file base name + with a '.webp' extension. If False, names the file using an MD5 hash of its + optimized data stream to ensure uniqueness and prevent duplicates. Defaults to False. + + Returns: + dict: A dictionary mapping original source URLs (extracted from corresponding '.ref' + files) to the newly generated file names (e.g., {'https://example.com': 'img_abc123.webp'}). + + Raises: + No explicit exceptions are raised. Errors encountered during individual file processing + or file mutations are intercepted and printed to standard output to allow the loop to continue. + """ + # Guard clause to skip image optimization processing runs if the source directory does not exist + if not os.path.exists(input_dir): + return {} + + # Safely creates the target destination output directory tree path mapping layers + os.makedirs(output_dir, exist_ok=True) + valid_extensions = (".jpg", ".jpeg", ".png", ".webp") # Tuple listing supported source assets to process + url_to_new_path_map = {} # Allocation map container linking original URLs to new filenames + + # Recursively step down through the local raw file directory storage tree structures + for root, _, files in os.walk(input_dir): + for file in files: + # Check if the file extension matches our list of target images + if file.lower().endswith(valid_extensions): + input_path = os.path.join(root, file) # Constructs the absolute system source path to the file + relative_path = os.path.relpath(root, input_dir) # Tracks internal subfolder offsets relative to root + target_folder = os.path.normpath(os.path.join(output_dir, relative_path)) # Formulates output directory + os.makedirs(target_folder, exist_ok=True) # Creates target subfolders if missing on disk + try: + # Instantiates a clean PIL workspace buffer tracking the source image file + with Image.open(input_path) as img: + img = ImageOps.exif_transpose(img) # Re-orients image automatically using structural EXIF metadata tags + if img.mode in ("P", "CMYK"): # Normalizes palette and print profiles to preserve alpha channels + img = img.convert("RGBA") + img.thumbnail(max_size, Image.Resampling.LANCZOS) # High-fidelity scale reduction utilizing Lanczos algorithm + buffer = BytesIO() # Initializes an in-memory byte pipeline stream buffer + img.save(buffer, format="WEBP", quality=quality) # Compresses and writes file into memory as WebP format + optimized_data = buffer.getvalue() # Extracts the optimized binary byte layout mapping data payload + + # Evaluates naming configurations to resolve output string file structures + if keep_original_names: + base_name, _ = os.path.splitext(file) # Separates filename strings from extensions + output_file_name = f"{base_name}.webp" + else: + hasher = hashlib.md5(optimized_data) # Instantiates MD5 engine against the unique file bits stream + content_hash = hasher.hexdigest() # Generates unique hex hash identifier mapping key + output_file_name = f"img_{content_hash}.webp" + + output_path = os.path.join(target_folder, output_file_name) # Builds output file destination block path + + # Guard clause checking local asset directories to ensure duplicate files are not overwritten + if not os.path.exists(output_path): + with open(output_path, "wb") as f: + f.write(optimized_data) # Commits byte layout data safely to permanent local storage disk + + # Delete the original raw file from temporary disk to preserve server space allocations + if os.path.exists(input_path): + os.remove(input_path) + + # Scan for `.ref` sidecar metadata maps containing the original source download URL address link + ref_path = os.path.join(root, f"{file}.ref") + if os.path.exists(ref_path): + with open(ref_path, "r") as ref_f: + orig_url = ref_f.read().strip() # Extracts original URL address string + url_to_new_path_map[orig_url] = output_file_name # Updates internal tracking links dictionary maps + + print(f"[Processed & Purged] {file} -> {output_file_name}") + except Exception as e: + print(f"Failed to process {file}. Error: {e}") # Log error messages to prevent entire automation pipeline crashes + return url_to_new_path_map # Returns the collection linking old asset links to new files + +def to_kebab(text): + """Converts a text string into a clean, lowercased kebab-case format + + suitable for slugs, URLs, or file names. + + The function strips leading/trailing whitespace, converts characters + to lowercase, removes non-alphanumeric symbols (excluding spaces + and hyphens), collapses consecutive spaces or hyphens into a single + dash, and trims leading or trailing dashes from the final output. + + Args: + text (str or None): The input text string to be slugified. + + Returns: + str: The transformed kebab-case string, or an empty string + if the input is empty or evaluates to None. + + Examples: + >>> to_kebab("Hello World!") + 'hello-world' + + >>> to_kebab(" My Awesome---Blog Post ") + 'my-awesome-blog-post' + + >>> to_kebab(None) + '' + """ + if not text: + return "" # Edge case mitigation handling missing properties gracefully + text = text.lower().strip() # Normalizes character cases and clips exterior whitespace frames + text = re.sub(r"[^a-z0-9\s-]", "", text) # Regex scrubbing punctuation and non-alphanumeric configuration patterns + return re.sub(r"[\s-]+", "-", text).strip("-") # Condenses multi-spaces into single hyphens and trims loose edges + +def main(): + """Executes the end-to-end extraction, asset optimization, and generation pipeline. + + This function acts as the orchestrator for the script. It loads the source JSON + payload from the environment variables, processes metadata, downloads + and compresses all remote image attachments into WebP files, and writes the final, formatted MDX + document directly to the workspace directory. + + Supported Form Input Fields (extracted from ISSUE_JSON): + title (str): The raw title text used for front matter and file naming. + description (str): A description of the new document that will be used in metadata + unit (str): unit with which to classify this document. + doc-content (str): Raw Markdown core narrative containing text and inline graphics. + Returns: + None + Raises: + KeyError: If the 'ISSUE_JSON' environment variable is entirely missing. + json.JSONDecodeError: If the provided configuration string contains malformed JSON data. + """ + + # --- SETUP FORMS & METADATA --- + # Guard clause asserting that the required input variable stream data block is active on the server + if "ISSUE_JSON" not in os.environ: + raise KeyError("Missing 'ISSUE_JSON' environment variable.") + + # Decodes incoming issue string parameters into interactive structural python dictionaries + data = json.loads(os.environ["ISSUE_JSON"]) + + # Get safe_title and raw_title from 'title' + raw_title = data.get("title", "Untitled Document").strip() # Pulls the document heading text from form mappings + description = data.get("description", "").strip() # Pulls the descriptive subtext metadata parameter string + + # Extract structural form input parameters from the loaded configuration map + selected_unit = data.get("unit", "General").strip() # Fetches target sorting classification group + doc_content = data.get("doc-content", "") # Extracts core multi-line markdown narrative layout text + + # Escape interior quote marks within text strings to prevent formatting breaks inside front matter metadata + safe_title = raw_title.replace('"', '\\"') + safe_description = description.replace('"', '\\"') + + # Transform raw title characters into a web-safe, standardized lower-case kebab-case text slug + slug = to_kebab(raw_title) + + # --- DETERMINE TARGET DIRECTORY PATH --- + # Convert chosen classification category name to a clean directory path format (e.g., "troop-331") + unit_folder = to_kebab(selected_unit) + docs_directory = f"docs/{unit_folder}" # Assembles path to the native Docusaurus document content tree + os.makedirs(docs_directory, exist_ok=True) # Provisions structural folder pathways on disk if missing + + # Set up static asset pipeline storage targets and frontend reference url strings + static_folder = f"static/img/docs/{unit_folder}/{slug}" # Permanent hard disk destination directory path + web_prefix = f"/img/docs/{unit_folder}/{slug}" # Client-side routing reference path used in generated web pages + + # --- PARSE AND DOWNLOAD INLINE IMAGES --- + # Regular expression capturing markdown image link variants, isolating raw matching pairs and clean URLs + inline_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', doc_content) + + # Conditional workflow block executing download pipelines if inline graphic links were identified + if inline_matches: + tmp_inline_dir = "/tmp/raw_doc_inline" # Defines isolated local runner workspace scratchpad buffer path + os.makedirs(tmp_inline_dir, exist_ok=True) # Safely constructs server workspace environment layer + + # Iterates through every unique index instance matching inline url string criteria + for index, (full_match_url, clean_match_url) in enumerate(inline_matches): + clean_url = clean_match_url.split("?")[0] # Discards query string parameters to protect clean path matching + _, ext = os.path.splitext(clean_url) # Extracts file extension out of the isolated tracking string + if not ext: + ext = ".png" # Default extension fallback parameter if undetected + + # Standardizes raw local filename text formats using iteration trackers + inline_filename = f"image_{index}{ext}" + inline_filepath = os.path.join(tmp_inline_dir, inline_filename) # Builds absolute path context link + + try: + # Dispatches server download request stream fetching remote assets directly onto the local drive + urllib.request.urlretrieve(clean_match_url, inline_filepath) + # Writes `.ref` sidecar files containing original absolute URL references to assist optimization mapping steps + with open(f"{inline_filepath}.ref", "w") as ref_f: + ref_f.write(clean_match_url) + except Exception as e: + print(f"Failed downloading inline image {clean_match_url}: {e}") # Non-blocking diagnostic tracker logging + + # Optimize downloads: scales, hashes, saves optimized assets, and removes temporary items from scratch space + inline_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) + + # Swap out raw URLs inside text area with local static paths + # Step down through calculated mapping dictionary records to update text body markup variables + for orig_url, webp_filename in inline_map.items(): + doc_content = doc_content.replace(orig_url, f"{web_prefix}/{webp_filename}") + + # --- BUILD FRONT MATTER AND WRITING THE MARKDOWN --- + markdown_path = f"{docs_directory}/{slug}.mdx" # Defines ultimate destination path for the document + + # Assemble metadata block required by Docusaurus + # Constructs a list array mapping standard key-value headers to fulfill Front Matter criteria + front_matter = [ + "---", + f'title: "{safe_title}"', + f'description: "{safe_description}"', + "---", + "", + ] + + # Merges compiled header metadata lists with modified markdown text blocks via joining newline strings + doc_payload = "\n".join(front_matter) + doc_content + + # Writes operational file contents back down to permanent project storage with universal UTF-8 file layouts + with open(markdown_path, "w", encoding="utf-8") as doc_file: + doc_file.write(doc_payload) + + print(f"[Pipeline Complete] Document successfully written to: {markdown_path}") + +if __name__ == "__main__": + main() # Executes the core application script runtime routine + diff --git a/.github/scripts/generate_post.py b/.github/scripts/generate_post.py new file mode 100644 index 0000000..6e0e89d --- /dev/null +++ b/.github/scripts/generate_post.py @@ -0,0 +1,457 @@ +# .github/scripts/generate_post.py +""" +Docusaurus Blog Post Generator and Asset Optimizer. + +This module automates the generation of MDX blog posts for Docusaurus from +structured GitHub Issue Form JSON data. It extracts front matter details +(title, date, authors, tags), downloads external asset attachments (cover +photos, inline body images, and multi-file photo albums), converts and compresses +images into responsive WebP formats, and injects custom React components. + +Workflow Steps: + 1. Parses form data and normalizes string strings into URL keys (kebab-case). + 2. Downloads and runs a multi-format image optimizer on cover configurations. + 3. Traverses inline markdown text to collect, downscale, and swap out local assets. + 4. Downloads sequential photo attachments and places them inside an album asset directory. + 5. Calculates text positions to cleanly inject a Docusaurus preview truncation marker. + 6. Compiles and exports a clean MDX file to the repository's native `blog/` folder. + +Environment Variables: + ISSUE_JSON (str): A stringified JSON object generated by `stefanbuck/github-issue-parser` + containing the raw form field mappings. + +Requirements: + - Pillow (PIL) + - python-dateutil + +File Structure Impact: + - Creates layout files at: `blog/{YYYY-MM-DD}-{slug}.mdx` + - Stores optimized assets at: `static/img/blog/{YYYY-MM-DD}-{slug}/` +""" +import os +import json +import re +import hashlib +import urllib.request +from io import BytesIO +from datetime import datetime +from dateutil import parser +from PIL import Image, ImageOps + +# --- Helper functions --- +def generate_clean_description(blog_content): + """Parses raw markdown content to extract a clean 150-character SEO description. + + Removes code blocks, images, headings, and markdown syntax markup to form + a punchy, reader-friendly string that fits within Google snippet limits. + + Args: + blog_content (str): The raw text string from the 'blog-content' form field. + + Returns: + str: A clean text summary exactly 150 characters or less with an ellipsis. + """ + if not blog_content or len(blog_content.strip()) < 15: + # High-utility localized fallback text if the body field was left empty + return "Discover recent outdoor adventures, volunteer service projects, and youth leadership milestones from our Brownsburg, IN Scouting units." + + # 1. Clean out the heaviest non-text elements completely + text = re.sub(r'```.*?```', '', blog_content, flags=re.DOTALL) # Remove code blocks + text = re.sub(r'!\[.*?\]\(.*?\)', '', text) # Remove markdown images + text = re.sub(r'', '', text) # Remove HTML images + text = re.sub(r'<.*?>.*?', '', text, flags=re.DOTALL) # Remove HTML tags/components + text = re.sub(r'#+\s+.*', '', text) # Remove headers (# Title) + + # 2. Strip out inline styling symbols + text = re.sub(r'[*_`~]', '', text) # Strip bold, italics, code strings, strikethroughs + text = re.sub(r'\[(.*?)\]\(.*?\)', r'\1', text) # Convert [Click Here](url) -> Click Here + text = re.sub(r'^[\-\*\+]\s+', '', text, flags=re.MULTILINE) # Strip out list item dashes/bullets + + # 3. Flatten lines breaks, tabs, and duplicate white space + text = " ".join(text.split()) + + # 4. Enforce strict 145-character boundary rule to protect against truncation + if len(text) > 145: + # Clip back to the last complete word so words aren't cut in half + text = text[:145].rsplit(' ', 1)[0].strip() + + # Ensure the sentence has an appropriate grammatical ending + if not text.endswith(('.', '!', '?')): + text += "..." + + return text + +def optimize_convert_and_hash_images(input_dir, output_dir, max_size=(1920, 1080), quality=80, keep_original_names=False): + """ + Optimizes, resizes, and converts images in an input directory to WebP format, + saving them to an output directory while optionally renaming them using MD5 hashes. + + This function traverses an input folder structure, processes supported images + (.jpg, .jpeg, .png, .webp), applies EXIF orientation fixes, resizes them to + fit within a maximum bounding box, and saves the output. It purges the original + files from disk upon successful processing. It also links processed files back + to their original reference URLs if matching '.ref' sidecar files are present. + + Args: + input_dir (str): Path to the directory containing raw source images. + output_dir (str): Path to the destination directory where WebP images will be saved. + max_size (tuple of int, optional): Maximum (width, height) bounding box for resizing + using a LANCZOS filter. Defaults to (1920, 1080). + quality (int, optional): Compression quality parameter for WebP export, + ranging from 1 to 100. Defaults to 80. + keep_original_names (bool, optional): If True, retains the original file base name + with a '.webp' extension. If False, names the file using an MD5 hash of its + optimized data stream to ensure uniqueness and prevent duplicates. Defaults to False. + + Returns: + dict: A dictionary mapping original source URLs (extracted from corresponding '.ref' + files) to the newly generated file names (e.g., {'https://example.com': 'img_abc123.webp'}). + + Raises: + No explicit exceptions are raised. Errors encountered during individual file processing + or file mutations are intercepted and printed to standard output to allow the loop to continue. + """ + if not os.path.exists(input_dir): + return {} + os.makedirs(output_dir, exist_ok=True) + valid_extensions = (".jpg", ".jpeg", ".png", ".webp") + url_to_new_path_map = {} + + for root, _, files in os.walk(input_dir): + for file in files: + if file.lower().endswith(valid_extensions): + input_path = os.path.join(root, file) + relative_path = os.path.relpath(root, input_dir) + target_folder = os.path.normpath(os.path.join(output_dir, relative_path)) + os.makedirs(target_folder, exist_ok=True) + try: + with Image.open(input_path) as img: + img = ImageOps.exif_transpose(img) + if img.mode in ("P", "CMYK"): + img = img.convert("RGBA") + img.thumbnail(max_size, Image.Resampling.LANCZOS) + buffer = BytesIO() + img.save(buffer, format="WEBP", quality=quality) + optimized_data = buffer.getvalue() + + if keep_original_names: + base_name, _ = os.path.splitext(file) + output_file_name = f"{base_name}.webp" + else: + hasher = hashlib.md5(optimized_data) + content_hash = hasher.hexdigest() + output_file_name = f"img_{content_hash}.webp" + + output_path = os.path.join(target_folder, output_file_name) + + if not os.path.exists(output_path): + with open(output_path, "wb") as f: + f.write(optimized_data) + + if os.path.exists(input_path): + os.remove(input_path) + + ref_path = os.path.join(root, f"{file}.ref") + if os.path.exists(ref_path): + with open(ref_path, "r") as ref_f: + orig_url = ref_f.read().strip() + url_to_new_path_map[orig_url] = output_file_name + + print(f"[Processed & Purged] {file} -> {output_file_name}") + except Exception as e: + print(f"Failed to process {file}. Error: {e}") + return url_to_new_path_map + +def to_kebab(text): + """ + Converts a text string into a clean, lowercased kebab-case format + suitable for slugs, URLs, or file names. + + The function strips leading/trailing whitespace, converts characters + to lowercase, removes non-alphanumeric symbols (excluding spaces + and hyphens), collapses consecutive spaces or hyphens into a single + dash, and trims leading or trailing dashes from the final output. + + Args: + text (str or None): The input text string to be slugified. + + Returns: + str: The transformed kebab-case string, or an empty string + if the input is empty or evaluates to None. + + Examples: + >>> to_kebab("Hello World!") + 'hello-world' + + >>> to_kebab(" My Awesome---Blog Post ") + 'my-awesome-blog-post' + + >>> to_kebab(None) + '' + """ + if not text: + return "" + text = text.lower().strip() + text = re.sub(r"[^a-z0-9\s-]", "", text) + return re.sub(r"[\s-]+", "-", text).strip("-") + +def main(): + """ + Executes the end-to-end extraction, asset optimization, and generation pipeline. + + This function acts as the orchestrator for the script. It loads the source JSON + payload from the environment variables, processes author metadata, downloads + and compresses all remote image attachments into WebP files, calculates layout + break positions for preview truncation, and writes the final, formatted MDX + blog post directly to the workspace directory. + + Supported Form Input Fields (extracted from ISSUE_JSON): + title (str): The raw title text used for front matter and file naming. + date (str, optional): Overriding target creation date. Defaults to current system date. + authors (str): Comma-separated list of author identifiers. + tags (str): Comma-separated list of classification metadata tags. + cover-photo (str, optional): Markdown link or plain URL pointing to a header image. + photo-album (str, optional): Clustered markdown links of gallery image attachments. + blog-content (str): Raw Markdown core narrative containing text and inline graphics. + + Returns: + None + + Raises: + KeyError: If the 'ISSUE_JSON' environment variable is entirely missing. + json.JSONDecodeError: If the provided configuration string contains malformed JSON data. + """ + # --- SETUP FORMS & METADATA --- + if "ISSUE_JSON" not in os.environ: + raise KeyError("Missing 'ISSUE_JSON' environment variable.") + + data = json.loads(os.environ["ISSUE_JSON"]) + + # Get safe_title and raw_title from 'title' + raw_title = data.get("title", "Untitled Post").strip() + safe_title = raw_title.replace('"', '\\"') + + # Get date_str from 'date' (convert whatever format was entered to YYYY-MM-DD) + form_date = data.get("date", "").strip() + current_time = datetime.now() + date_str = current_time.strftime("%Y-%m-%d") + + if form_date and form_date != "null" and form_date != "_No response_": + try: + year_anchor = datetime(current_time.year, 1, 1) + parsed_date = parser.parse(form_date, default=year_anchor, fuzzy=True) + date_str = parsed_date.strftime("%Y-%m-%d") + except Exception as e: + print(f"[Date Parsing Alert] Fallback used. Error: {e}") + + # Get blogfilename from date_str and slugified raw_title + slug = to_kebab(raw_title) + blogfilename = f"{date_str}-{slug}" + + # Get list of raw_authors from 'authors' + raw_authors = data.get("authors", "") + if isinstance(raw_authors, str): + authors = [to_kebab(a) for a in raw_authors.split(",") if a.strip()] + else: + authors = [to_kebab(str(raw_authors))] + + # Get raw_tags from kebabed tags (Unit number) + raw_tags = data.get("tags", "") + if isinstance(raw_tags, str): + tags = [to_kebab(t) for t in raw_tags.split(",") if t.strip()] + else: + tags = [to_kebab(str(raw_tags))] + + # File locations for processed images + static_folder = f"static/img/blog/{blogfilename}" + web_prefix = f"/img/blog/{blogfilename}" + + # --- FETCH AND RUN OPTIMIZER ON COVER PHOTO --- + cover_input = data.get("cover-photo") + cover_match = re.search(r'\]\((https?://[^\s\)]+)\)', cover_input) + cover_url = cover_match.group(1) if cover_match else (cover_input.strip() if cover_input.strip().startswith(('http://', 'https://')) else None) + + cover_line = "" + if cover_url and cover_url != "null" and cover_url != "_No response_": + tmp_cover_dir = "/tmp/raw_cover" + os.makedirs(tmp_cover_dir, exist_ok=True) + clean_url = cover_url.split("?") + _, ext = os.path.splitext(clean_url[0]) + if not ext: + ext = ".jpg" + raw_cover_path = os.path.join(tmp_cover_dir, f"cover{ext}") + try: + urllib.request.urlretrieve(cover_url, raw_cover_path) + with open(f"{raw_cover_path}.ref", "w") as ref_f: + ref_f.write("cover") + optimize_convert_and_hash_images(tmp_cover_dir, static_folder, keep_original_names=True) + cover_line = f"![Cover Picture]({web_prefix}/cover.webp)" + except Exception as e: + print(f"Skipping cover photo download. Error: {e}") + + # --- FETCH AND RUN OPTIMIZER ON ALL ALBUM SLIDES --- + album_input = data.get("photo-album", "") + # Extract all markdown image links from the multi-file upload field + album_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', album_input) + has_album = False + + if album_matches: + tmp_slides_dir = "/tmp/raw_slides" + os.makedirs(tmp_slides_dir, exist_ok=True) + + # Nest the optimized slides directly inside the target dynamic subfolder path + slides_static_folder = os.path.join(static_folder, "slides") + + # Download each individual photo into the temporary processing directory + for index, (full_match_url, clean_match_url) in enumerate(album_matches): + clean_url = clean_match_url.split("?")[0] + _, ext = os.path.splitext(clean_url) + if not ext: + ext = ".png" + + # Use an index-based name to keep them unique during raw staging + album_filename = f"album_{index}{ext}" + album_filepath = os.path.join(tmp_slides_dir, album_filename) + + try: + urllib.request.urlretrieve(clean_match_url, album_filepath) + + # Create the required sidecar reference file for tracking + with open(f"{album_filepath}.ref", "w") as ref_f: + ref_f.write(clean_match_url) + except Exception as e: + print(f"Failed downloading album image {clean_match_url}: {e}") + + # Process and optimize the directory full of individual files + optimize_convert_and_hash_images(tmp_slides_dir, slides_static_folder, keep_original_names=False) + has_album = True + + # --- PARSE AND DOWNLOAD IMAGES INSIDE BLOG CONTENT --- + blog_content = data.get("blog-content", "") + # Find all markdown image URLs inside the text editor field + inline_matches = re.findall(r'!\[.*?\]\(((https?://[^\s\)]+))\)', blog_content) + + if inline_matches: + tmp_inline_dir = "/tmp/raw_inline" + os.makedirs(tmp_inline_dir, exist_ok=True) + + for index, (full_match_url, clean_match_url) in enumerate(inline_matches): + clean_url = clean_match_url.split("?") + _, ext = os.path.splitext(clean_url[0]) + if not ext: + ext = ".png" + inline_filename = f"inline_{index}{ext}" + inline_filepath = os.path.join(tmp_inline_dir, inline_filename) + + try: + urllib.request.urlretrieve(clean_match_url, inline_filepath) + with open(f"{inline_filepath}.ref", "w") as ref_f: + ref_f.write(clean_match_url) + except Exception as e: + print(f"Failed downloading inline image {clean_match_url}: {e}") + + inline_map = optimize_convert_and_hash_images(tmp_inline_dir, static_folder, keep_original_names=False) + + # Swap original markdown URLs with the newly generated WebP image routes + for orig_url, webp_filename in inline_map.items(): + blog_content = blog_content.replace(orig_url, f"{web_prefix}/{webp_filename}") + + # --- NEW: GENERATE THE AUTOMATED SEO DESCRIPTION --- + # Extracts text out of the final modified content string right before writing the file + seo_description = generate_clean_description(blog_content) + + # --- WRITE OUT THE MDX POST FILE --- + os.makedirs("blog", exist_ok=True) + mdx_filepath = f"blog/{blogfilename}.mdx" + + front_matter_authors = ", ".join(authors) + front_matter_tags = ", ".join(tags) + + # Inject the dynamic PhotoAlbumGallery component into the template if an album was uploaded + import_line = "" + album_component_markdown = "" + if has_album: + import_line = "import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery';\n\n" + album_component_markdown = f"""\n\n## Photo Album\n\n\n""" + + # Handle Docusaurus preview truncation, ignoring non-text layout blocks + # Split by double newlines to isolate paragraphs + MAX_PARAGRAPH_CHARS = 350 # Fallback limit for excessively long paragraphs + paragraphs = blog_content.split("\n\n") + + first_text_index = None + has_subsequent_content = False + + # Step A: Locate the first true text block that isn't an image, component, or import statement + for idx, p in enumerate(paragraphs): + stripped_p = p.strip() + + # Combined skip condition for empty blocks, images, code, lists, and headings + if (not stripped_p or + stripped_p.startswith(("![", " MAX_PARAGRAPH_CHARS: + # Find the closest sentence end near our maximum character threshold + # It scans for periods, exclamation marks, or question marks followed by a space + sentence_ends = [m.end() for m in re.finditer(r'[\.\!\?]\s', target_paragraph[:MAX_PARAGRAPH_CHARS + 50])] + + if sentence_ends: + # Split at the last complete sentence within our threshold window + split_point = sentence_ends[-1] + truncated_part = target_paragraph[:split_point].strip() + remaining_part = target_paragraph[split_point:].strip() + + # Re-structure the paragraph to house the tag internally + paragraphs[first_text_index] = f"{truncated_part}\n\n{{/* truncate */}}\n\n{remaining_part}" + else: + # Hard fallback if no punctuation is found: split exactly at the character count + truncated_part = target_paragraph[:MAX_PARAGRAPH_CHARS].strip() + remaining_part = target_paragraph[MAX_PARAGRAPH_CHARS:].strip() + paragraphs[first_text_index] = f"{truncated_part}...\n\n{{/* truncate */}}\n\n...{remaining_part}" + + # If the paragraph is normal length and there's more text coming later, append tag after it + elif has_subsequent_content: + paragraphs.insert(first_text_index + 1, "{/* truncate */}") + + blog_content = "\n\n".join(paragraphs) + + with open(mdx_filepath, "w", encoding="utf-8") as mdx_file: + mdx_file.write(f"---\n") + mdx_file.write(f"title: \"{safe_title}\"\n") + mdx_file.write(f"description: \"{seo_description}\"\n") + mdx_file.write(f"date: {date_str}\n") + if authors: + mdx_file.write(f"authors: [{front_matter_authors}]\n") + if tags: + mdx_file.write(f"tags: [{front_matter_tags}]\n") + if cover_line: + mdx_file.write(f"image: {web_prefix}/cover.webp\n") + mdx_file.write(f"---\n\n") + if import_line: + mdx_file.write(import_line) + if cover_line: + mdx_file.write(f"{cover_line}\n\n") + mdx_file.write(f"{blog_content}") + mdx_file.write(album_component_markdown) + + print(f"Successfully generated blog file at: {mdx_filepath}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/blog-to-facebook.yml b/.github/workflows/blog-to-facebook.yml new file mode 100644 index 0000000..d8d1cd0 --- /dev/null +++ b/.github/workflows/blog-to-facebook.yml @@ -0,0 +1,92 @@ +# ============================================================================== +# GitHub Actions Automation Workflow: Multi-Target Blog Post Router to Facebook +# Automatically triggers when code commits are pushed into main containing blog modifications. +# Leverages an external Python tracking script to build a dynamic target matrix array, +# then spawns parallel posting steps targeting Troop 303, Troop 331, Pack 303, or Crew 303 pages. +# ============================================================================== + +name: Route Blog Posts to Facebook Pages # Friendly workflow tracking name shown in Actions panel + +on: # Defines the system event trigger hooks + push: # Listens to push event blocks across the ecosystem + branches: + - main # Restricts execution to the master production code trunk line branch + paths: + - 'blog/**' # Limits trigger conditions to file modifications inside the blog directory tree + +jobs: # Begins structural job step pipeline workflows + # ---------------------------------------------------------------------------- + # JOB 1: Discover modification targets and serialize metadata values + # ---------------------------------------------------------------------------- + parse-blog: + runs-on: ubuntu-latest # Provisions a fresh virtual machine Linux runner workspace environment + outputs: # Maps global outputs from local tracking steps to make them accessible across jobs + has_new_post: ${{ steps.blog_data.outputs.has_new_post }} + title: ${{ steps.blog_data.outputs.title }} + slug: ${{ steps.blog_data.outputs.slug }} + targets: ${{ steps.blog_data.outputs.targets }} # Contains the stringified JSON target array e.g., ["TROOP_303", "PACK_303"] + steps: + - name: Checkout repository # Step 1: Fetches repository source documentation onto the runner platform + uses: actions/checkout@v4 + with: + fetch-depth: 2 # Forces checking history depths of 2 commits so git diff can track HEAD~1 shifts + + - name: Set up Python # Step 2: provisions Python interpreter infrastructure binaries + uses: actions/setup-python@v5 + with: + python-version: '3.x' # Always downloads the latest functional patch build iteration of Python 3 + + - name: Parse Metadata via Python # Step 3: Run router script to identify targeting parameters + id: blog_data # Step identifier token used by outputs map fields to find variables + run: python .github/scripts/fb_router.py + + # ---------------------------------------------------------------------------- + # JOB 2: Run dynamic multi-process loops to dispatch entries onto Facebook Pages + # ---------------------------------------------------------------------------- + facebook-routing: + needs: parse-blog # Inter-job dependency lock: halts execution until data collection completes + # Execution gate: only runs if a post was successfully identified and targets list array contains entries + if: needs.parse-blog.outputs.has_new_post == 'true' && needs.parse-blog.outputs.targets != '[]' + runs-on: ubuntu-latest # Provisions an independent parallel system container node + strategy: + fail-fast: false # Prevent crash dependencies: keeps other unit operations processing if one page token fails + matrix: + # Converts the Python string output back into a live GitHub Actions looping engine array schema + target: ${{ fromJson(needs.parse-blog.outputs.targets) }} + steps: + - name: Set Facebook Credentials Dynamically # Step 1: Conditional evaluation mapping loop tokens to secure Vault entries + id: set_tokens # ID marker allowing the cURL step to fetch the selected dynamic parameters + run: | + TARGET="${{ matrix.target }}" # Extract current matrix iteration string item token value + if [ "$TARGET" = "TROOP_331" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_TROOP_331 }}" >> $GITHUB_OUTPUT # Maps the unique identification numbers + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_TROOP_331 }}" >> $GITHUB_OUTPUT # Maps the specific system token + elif [ "$TARGET" = "PACK_303" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_PACK_303 }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_PACK_303 }}" >> $GITHUB_OUTPUT + elif [ "$TARGET" = "CREW_303" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_CREW_303 }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_CREW_303 }}" >> $GITHUB_OUTPUT + elif [ "$TARGET" = "TROOP_303" ]; then + echo "page_id=${{ secrets.FB_PAGE_ID_TROOP_303 }}" >> $GITHUB_OUTPUT + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_TROOP_303 }}" >> $GITHUB_OUTPUT + else + echo "page_id=${{ secrets.FB_PAGE_ID_DEFAULT }}" >> $GITHUB_OUTPUT # Fallback mappings if special exceptions occur + echo "access_token=${{ secrets.FB_ACCESS_TOKEN_DEFAULT }}" >> $GITHUB_OUTPUT + fi + + - name: Send Link to Selected Facebook Page # Step 2: Dispatches feed payload securely onto the Facebook Graph API endpoints + run: | + # Formulates absolute permalink layout references ensuring the /blog directory prefix matches your structure + BLOG_URL="https://brownsburgscouts.org{{ needs.parse-blog.outputs.slug }}" + + # Sets up text message string template payloads using emoticons and line skip variables + MESSAGE="✍️ New Blog Post: ${{ needs.parse-blog.outputs.title }} + + Read the full article here: $BLOG_URL" + + # Fires cURL POST web request arguments hitting Meta Graph API servers, routing items onto dynamic destinations + curl -X POST "https://facebook.com{{ steps.set_tokens.outputs.page_id }}/feed" \ + -d "message=$MESSAGE" \ + -d "link=$BLOG_URL" \ + -d "access_token=${{ secrets.FB_ACCESS_TOKEN_DEFAULT }}" # Authorizes operations utilizing isolated access tokens diff --git a/.github/workflows/content-management.yml b/.github/workflows/content-management.yml new file mode 100644 index 0000000..d47c6c6 --- /dev/null +++ b/.github/workflows/content-management.yml @@ -0,0 +1,244 @@ +# ============================================================================== +# GitHub Actions Automation Workflow: Content Management +# Orchestrates automatic Pull Request creation when issues with specific labels +# ('blog', 'docs', or 'new-author') are submitted via issue form templates. +# ============================================================================== + +name: Content Management Automation # Friendly name displayed in the GitHub Actions tab + +on: # Defines the event hook that triggers this workflow + issues: # Listens to issue activity + types: [opened] # Restricts execution to run only when an issue is newly created + +jobs: # Contains the operational pipelines/jobs to execute + # ---------------------------------------------------------------------------- + # WORKFLOW 1: Generate Blog Post from Issue + # ---------------------------------------------------------------------------- + create-post: + # Execution gate: only runs if the newly opened issue has the 'blog' label applied + if: contains(github.event.issue.labels.*.name, 'blog') + runs-on: ubuntu-latest # Provisions a fresh virtual machine host environment + steps: + - name: Checkout Code # Step 1: Pulls down your repository code onto the runner + uses: actions/checkout@v6 # Uses the standard repository checkout action + + - name: Generate GitHub App Token # Step 2: Obtains elevated permissions for automation steps + id: app-token # ID designation used to query output data in later fields + uses: actions/create-github-app-token@v3 # Uses GitHub App credentials to bypass workflow limits + with: + client-id: ${{ secrets.APP_ID }} # References system secret credentials + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Parse Issue Form # Step 3: Extracts individual form values from the issue + id: parse # ID notation used to fetch variables like jsonString + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: .github/ISSUE_TEMPLATE/01-new-blog-post.yml # Path targeting the source form template + + - name: Set up Python # Step 4: Installs runtime environment for processing scripts + uses: actions/setup-python@v6 + with: + python-version: "3.x" # Always provisions the latest minor build variation of Python 3 + + - name: Install Dependencies # Step 5: Installs mandatory libraries for image/text handling + run: | + python -m pip install --upgrade pip + pip install Pillow python-dateutil + + - name: Process Issue Data and Assets (Python Optimizer) # Step 6: Executes data conversion script + id: process + env: + ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} # Maps extracted form fields to environment variables + run: | + python .github/scripts/generate_post.py # Script execution target file + + - name: Create Pull Request # Step 7: Opens a staging PR with the new post files + id: cpr # ID mapping used to fetch the PR URL for issue commenting + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.app-token.outputs.token }} # Grants PR permissions via the generated App token + commit-message: "feat(blog): add new post from issue #${{ github.event.issue.number }}" + branch: "automation/issue-${{ github.event.issue.number }}-${{ env.BLOG_FILENAME }}" # Isolated branch path + title: "feat(blog): ${{ github.event.issue.title }}" # Sets the issue title as the PR title + body: | # Description text posted within the generated PR body description + This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. + Closes #${{ github.event.issue.number }} + delete-branch: true # Deletes the branch once the PR is merged into main + labels: | # Automatically appends tracking tags to the PR + blog + + - name: Comment on Issue with PR Link # Step 8: Provides clear notification link back to the user + if: steps.cpr.outputs.pull-request-number != '' # Guard clause ensuring the PR was successfully opened + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | # Formats notice text targeting the native conversation section + πŸŽ‰ Success! A staging branch has been created. + + Your blog post is ready for review here:πŸ‘‰ ${{ steps.cpr.outputs.pull-request-url }} + + # ---------------------------------------------------------------------------- + # WORKFLOW 2: Generate Document from Issue + # ---------------------------------------------------------------------------- + create-doc: + # Execution gate: only runs if the newly opened issue has the 'docs' label applied + if: contains(github.event.issue.labels.*.name, 'docs') + runs-on: ubuntu-latest # Provisions a fresh virtual machine host environment + steps: + - name: Checkout Code # Step 1: Pulls down your repository code onto the runner + uses: actions/checkout@v6 + + - name: Generate GitHub App Token # Step 2: Obtains elevated permissions for automation steps + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Parse Issue Form # Step 3: Extracts individual form values from the issue + id: parse + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: .github/ISSUE_TEMPLATE/02-new-document.yml # Targeting the document form template + + - name: Set up Python # Step 4: Installs runtime environment for processing scripts + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install Dependencies # Step 5: Installs mandatory libraries for file handling + run: | + python -m pip install --upgrade pip + pip install Pillow + + - name: Process Issue Data and Assets (Python Optimizer) # Step 6: Executes file conversion script + id: process + env: + ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} # Maps extracted form fields to environment variables + run: | + python .github/scripts/generate_docs.py + + - name: Create Pull Request # Step 7: Opens a staging PR with the new documentation files + id: cpr + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "feat(docs): add new doc from issue #${{ github.event.issue.number }}" + branch: "automation/issue-${{ github.event.issue.number }}-${{ env.DOC_FILENAME }}" + title: "feat(docs): ${{ github.event.issue.title }}" + body: | + This Pull Request was automatically generated from Issue #${{ github.event.issue.number }}. + Closes #${{ github.event.issue.number }} + delete-branch: true + labels: | + docs + + - name: Comment on Issue with PR Link # Step 8: Provides clear notification link back to the user + if: steps.cpr.outputs.pull-request-number != '' + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | + πŸŽ‰ Success! A staging branch has been created. + + Your document is ready for review here:πŸ‘‰ ${{ steps.cpr.outputs.pull-request-url }} + + # ---------------------------------------------------------------------------- + # WORKFLOW 3: Process New Author Request + # ---------------------------------------------------------------------------- + add-author-and-pr: + # Execution gate: only runs if the newly opened issue has the 'new-author' label applied + if: contains(github.event.issue.labels.*.name, 'new-author') + runs-on: ubuntu-latest # Provisions a fresh virtual machine host environment + steps: + - name: Checkout Repo # Step 1: Pulls down your repository code onto the runner + uses: actions/checkout@v6 + + - name: Generate Token # Step 2: Obtains elevated permissions for automation steps + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Parse Issue Form # Step 3: Extracts profile fields from onboarding issue + id: parse + uses: stefanbuck/github-issue-parser@v3 + with: + template-path: .github/ISSUE_TEMPLATE/03-new-blog-author.yml # Targeting onboarding template + + - name: Set up Python # Step 4: Installs runtime environment for onboarding script + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install Dependencies # Step 5: Installs text/image optimization libraries + run: | + python -m pip install --upgrade pip + pip install Pillow PyYAML python-dateutil + + - name: Process Author and Update Files # Step 6: Appends dataset and edits dropdown lists + id: process_files + env: + ISSUE_DATA: ${{ steps.parse.outputs.jsonString }} # Context identifier variable name target + run: | + python .github/scripts/add_author.py + + - name: Create Pull Request # Step 7: Opens a staging PR updating configuration maps + if: success() # Condition check: ensures data steps finished with exit code 0 + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ steps.app-token.outputs.token }} + commit-message: "feat: add new blog author via Issue #${{ github.event.issue.number }}" + title: "feat: add new author from Issue #${{ github.event.issue.number }}" + body: | + This PR automatically handles three tasks: + 1. Downloads, optimizes, and names the avatar image matching the unique author slug. + 2. Adds the new author data mapping properties into blog/authors.yml. + 3. Re-generates and sorts the author dropdown options inside the issue templates. + Closes #${{ github.event.issue.number }}. + CODEOWNERS have been automatically assigned to review. + branch: "automation/issue-${{ github.event.issue.number }}" + delete-branch: true + labels: | + new-author + + - name: Comment on Success # Step 8: Alerts author that review process has begun + if: success() # Condition check: executes only if PR creation succeeds + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ steps.app-token.outputs.token }} + issue-number: ${{ github.event.issue.number }} + body: | + Hi there! An automated Pull Request has been generated to add you to + the blog authors file and update our submission dropdown tools. The + project's CODEOWNERS have been notified to review and merge the + changes. Once merged, your author profile and dropdown options will + be active! + + - name: Handle Duplicate / Failure # Step 9: Rejects entry and handles automated clean close + if: failure() # Condition check: executes if scripts fail (e.g., duplicate errors) + uses: actions/github-script@v9 # Leverages native JavaScript inline engine execution tools + with: + github-token: ${{ secrets.GITHUB_TOKEN }} # Standard repo token is fine for closure actions + script: | # Runs inline Javascript logic against Github API layers + const issueNumber = context.issue.number; + // Post notification error explanation comment to issue chat log + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: "πŸ›‘ Registration Error: It looks like an author profile with + this name already exists in blog/authors.yml. Duplicate entries are + not allowed. This issue will now be closed automatically." + }); + // Immediately close out the issue and label it as uncompleted + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: 'closed', + state_reason: 'not_planned' + }); \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c31655f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +// .vscode/extensions.json +{ + "recommendations": [ + "esbenp.prettier-vscode", + "streetsidesoftware.code-spell-checker", + "DavidAnson.vscode-markdownlint", + "ms-python.python", + "redhat.vscode-yaml", + "github.vscode-github-actions", + "GitHub.vscode-pull-request-github" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d24dd08..e3e219f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ +// .vscode/settings.json { "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode", @@ -18,5 +19,7 @@ "enabled": false } }, - "cSpell.enabled": true + "cSpell.enabled": true, + "editor.tabSize": 2, + "editor.detectIndentation": false } \ No newline at end of file diff --git a/README.md b/README.md index b3ee600..245c099 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,35 @@ -# The Scouting Units of American Legion Post 331 +# The Scouting Units of American Legion Post 331 ⚜️ -This repository hosts the content for the Website for the Scouting Units of -American Legion Post 331. +Welcome to the official code repository for The Scouting Units of American +Legion Post 331 website. -## Updating Site +## 🎯 Purpose of this Repository -### Homepage +This repository hosts the source code, content, and configuration files for our +unit's community website. The site is built using +[Docusaurus](https://docusaurus.io), a modern static site generator. This +platform makes content creation easy and straight forward. -Other than images, the homepage should stay fairly static. +The primary goals of this website are to: -#### Carousel +- **Inform Families:** Provide parents and scouts with easy access to schedules, + packing lists, policies and information regarding our units. +- **Streamline Operations:** Host downloadable permission slips, medical forms, + and resource links. +- **Share Our Story:** Show prospective members what makes our units a great + place to grow, learn, and experience the outdoors. -The carousel pictures are stored in `/static/img/carousel/`. The files shall be -jpg images. When replacing pictures, keep the names the same (older photos are -automatically archived in the git repo). +--- -#### Feature Cards +## ✍️ How to Contribute -The feature card pictures are stored in `/static/img/feature-cards/`. The files -should be jpg images. When replacing the pictures, keep the name the same. +We encourage scouts, parents, and adult leaders to help keep our website +accurate and up-to-date! -### Docs +To protect our website's integrity and layout, all updatesβ€”including fixing +typos, adding calendar events, or uploading new documentsβ€”must follow our +standard workflow. -Each unit has its own docs locations. All of these are "markdown" files. Adding -documents is as simple as adding the new markdown file to the folder in -`/docs/` directory. - -### Blog - -These are also all markdown files. Tags can be added to associate it with a particular -unit. - -## List of TODOs - -- [ ] Determine list of domains (subdomains can also be used) - -### Homepage TODOs - -- [ ] Determine what links should be in navbar -- [ ] Determine best pictures - -### Docs pages TODOs - -- [ ] Collect content for all units that they want displayed -- [ ] Determine organization (what docs should be in general category) +Please read our **[CONTRIBUTING.md](CONTRIBUTING.md)** document for full +instructions on setting up your local environment, writing markdown content, and +submitting your changes for review. diff --git a/blog/2026/04/27/western-camporee/index.mdx b/blog/2026-04-27-western-camporee.mdx similarity index 85% rename from blog/2026/04/27/western-camporee/index.mdx rename to blog/2026-04-27-western-camporee.mdx index 158ccbe..66c4c3d 100644 --- a/blog/2026/04/27/western-camporee/index.mdx +++ b/blog/2026-04-27-western-camporee.mdx @@ -1,12 +1,14 @@ --- title: Western Division Spring Camporee -authors: [ben-s] +date: 2026-04-27 +authors: [benjamin-shover] tags: [troop-303] +image: /img/blog/2026-04-27-western-camporee/cover.webp --- import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery'; -![Departure Picture](/img/blog/2026/04/27/western-camporee/cover.webp) +![Cover Photo](/img/blog/2026-04-27-western-camporee/cover.webp) The Scouts of the Legendary Troop 303 attended the 2026 Western Division Spring Camporee this past weekend. @@ -33,5 +35,5 @@ Overall, a great weekend was had by all who attended and the troop looks forward to future adventures! diff --git a/blog/2026/04/28/railroading/index.mdx b/blog/2026-04-28-railroading.mdx similarity index 64% rename from blog/2026/04/28/railroading/index.mdx rename to blog/2026-04-28-railroading.mdx index b5d463e..b7b63a3 100644 --- a/blog/2026/04/28/railroading/index.mdx +++ b/blog/2026-04-28-railroading.mdx @@ -1,10 +1,12 @@ --- title: Railroading Camporee -authors: [ben-s] +date: 2026-04-28 +authors: [benjamin-shover] tags: [troop-331] +image: /img/blog/2026-04-28-railroading/cover.webp --- -![Blog Cover Photo](/img/blog/2026/04/28/railroading/cover.webp) +![Cover Photo](/img/blog/2026-04-28-railroading/cover.webp) The Scouts of Troop 331 went out of council to attend the Hoosier Trails Council's Railroading Camporee. diff --git a/blog/2026/06/03/spring-coh/index.mdx b/blog/2026-06-03-spring-coh.mdx similarity index 87% rename from blog/2026/06/03/spring-coh/index.mdx rename to blog/2026-06-03-spring-coh.mdx index d942b65..998bf60 100644 --- a/blog/2026/06/03/spring-coh/index.mdx +++ b/blog/2026-06-03-spring-coh.mdx @@ -1,6 +1,7 @@ --- title: Spring Court of Honor -authors: [ben-s] +date: 2026-06-03 +authors: [benjamin-shover] tags: [troop-303, troop-331] --- @@ -21,5 +22,5 @@ Congrats to all of the Scouts who earned awards! Be sure to look through the image gallery to see all the highlights from the evening. diff --git a/blog/2026/06/09/scoutmaster-blog/index.mdx b/blog/2026-06-09-scoutmasters-blog.mdx similarity index 90% rename from blog/2026/06/09/scoutmaster-blog/index.mdx rename to blog/2026-06-09-scoutmasters-blog.mdx index 4938032..3c91b95 100644 --- a/blog/2026/06/09/scoutmaster-blog/index.mdx +++ b/blog/2026-06-09-scoutmasters-blog.mdx @@ -1,5 +1,6 @@ --- title: "Day 1: A Scoutmaster's Blog" +date: 2026-06-09 authors: [chris-koczan] tags: [troop-303] --- @@ -7,11 +8,13 @@ tags: [troop-303] A bit chaotic as many people and appointments have to line up just to be a good day. There was some misalignment, but in the end I got situated in my room, had a PICC line inserted (this goes into a bigger artery/vein for easier access to -the heart's pumping acting. Chemotherapy needs to be delivered into the big A/V +the heart's pumping acting). Chemotherapy needs to be delivered into the big A/V areas so it does not get stuck in the smaller areas. This ensure better delivery to where it is needed. Then rest, as chemotherapy could not begin before the ECHO of the heart to make sure it was healthy and able to receive the chemo. +{/* truncate */} + It was awesome to see Mr. Beasley and to pray with him over me. Scouts, you truly know who cares for you when your on a tough road. We got to talk about the journey so far and even remembered several Scouting events and had a good laugh. diff --git a/blog/2026/06/10/scoutmaster-blog/index.mdx b/blog/2026-06-10-scoutmasters-blog.mdx similarity index 57% rename from blog/2026/06/10/scoutmaster-blog/index.mdx rename to blog/2026-06-10-scoutmasters-blog.mdx index a0fc8af..f06554e 100644 --- a/blog/2026/06/10/scoutmaster-blog/index.mdx +++ b/blog/2026-06-10-scoutmasters-blog.mdx @@ -1,5 +1,6 @@ --- title: "Day 2: A Scoutmaster's Blog" +date: 2026-06-10 authors: [chris-koczan] tags: [troop-303] --- @@ -9,12 +10,15 @@ came next which included all of the anti-nausea medications to help get through the chemotherapy. Chemo came at 1500 and was not as bad as I had thought. Fifteen minutes for one, then an hour for the second. Today it was Mr. Darland and Mr. McConnell. Kudos to both of them, hospitals tend to make people nervous -because they don't know what to say or do. To be perfectly honest, just your -presence makes all the difference in the world. We laughed and enjoy time -together, and Mrs. McConnell made me some of her famous baked oyster crackers -and seasonings. We refer to this as Scoutmaster Crack, just because of how good -it is and how fast it disappears. +because they don't know what to say or do. + +{/* truncate */} + +To be perfectly honest, just your presence makes all the difference in the +world. We laughed and enjoyed time together, and Mrs. McConnell made me some of +her famous baked oyster crackers and seasonings. We refer to this as Scoutmaster +Crack, just because of how good it is and how fast it disappears. After chemo, more fluids to keep the Kidneys flushed out and to help move the -chemo along once it has done it's job. Release day is scheduled for Friday, +chemo along once it has done its job. Release day is scheduled for Friday, though given other delays we will wait and see. diff --git a/blog/2026-06-17-grant-b.mdx b/blog/2026-06-17-grant-b.mdx index 5d68867..f4f6ea3 100644 --- a/blog/2026-06-17-grant-b.mdx +++ b/blog/2026-06-17-grant-b.mdx @@ -1,8 +1,9 @@ --- title: "Congratulations to Grant B" date: 2026-06-17 -authors: [ben-s] +authors: [benjamin-shover] tags: [troop-303] +image: /img/blog/2026-06-17-grant-b/cover.webp --- import PhotoAlbumGallery from '@site/src/components/PhotoAlbumGallery'; diff --git a/blog/authors.yml b/blog/authors.yml index ed2439e..73c6533 100644 --- a/blog/authors.yml +++ b/blog/authors.yml @@ -11,21 +11,19 @@ # * # * Optional Schema Attributes: # * - image_url: Remote path string pointing to avatar images (e.g., GitHub profile avatars). -# * - url: Web address link to an external personal portfolio or professional landing profile. -# * - email: Direct mailbox address endpoint for personal messaging inquiries. # * # * @environment YAML Data Configuration Schema # * @see {@link https://docusaurus.io | Docusaurus Blog Authors Documentation} # */ -ben-s: - name: Benjamin S - title: Assistant Scoutmaster, Committee Member - image_url: https://github.com/shoverbj.png +benjamin-shover: + name: Benjamin Shover + title: Assistant Scoutmaster Troop 303, Committee Member + image_url: /img/blog/authors/benjamin-shover.png page: true chris-koczan: - name: Christopher Koczan + name: Chris Koczan title: Scoutmaster, Troop 303 image_url: /img/blog/authors/chris-koczan.jpg page: true \ No newline at end of file diff --git a/cookbook/breakfast/biscuit-gravy.mdx b/cookbook/breakfast/biscuit-gravy.mdx index a81e72b..b2e8562 100644 --- a/cookbook/breakfast/biscuit-gravy.mdx +++ b/cookbook/breakfast/biscuit-gravy.mdx @@ -1,7 +1,6 @@ --- title: Biscuits and Gravy description: Hearty meal of fresh baked biscuits and creamy sausage gravy -sidebar_position: 1 --- ## Ingredients diff --git a/cookbook/dutch-oven.mdx b/cookbook/dutch-oven.mdx index 9fafdc0..d6176b0 100644 --- a/cookbook/dutch-oven.mdx +++ b/cookbook/dutch-oven.mdx @@ -1,6 +1,5 @@ --- title: Dutch Oven Cooking -sidebar_position: 6 --- ## Why "Dutch Oven" diff --git a/cookbook/index.mdx b/cookbook/index.mdx index 2a49e09..c113d90 100644 --- a/cookbook/index.mdx +++ b/cookbook/index.mdx @@ -1,6 +1,5 @@ --- title: "Camping Recipes" -sidebar_position: 1 --- From hotdogs and s'mores over a campfire, to true gourmet delights, cooking diff --git a/docs/general-docs/health-form.mdx b/docs/general/health-form.mdx similarity index 100% rename from docs/general-docs/health-form.mdx rename to docs/general/health-form.mdx diff --git a/docs/general-docs/helpful-links.mdx b/docs/general/helpful-links.mdx similarity index 100% rename from docs/general-docs/helpful-links.mdx rename to docs/general/helpful-links.mdx diff --git a/docs/general-docs/merit-badge-counselor.mdx b/docs/general/merit-badge-counselor.mdx similarity index 100% rename from docs/general-docs/merit-badge-counselor.mdx rename to docs/general/merit-badge-counselor.mdx diff --git a/docusaurus.config.js b/docusaurus.config.js index a78f778..cb49e34 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,9 +1,9 @@ /** * @file docusaurus.config.js * @description Master Node.js configuration schema engine for the American Legion Post 331 Scouting website. - * Declares localized metadata paths, custom theme configurations, multi-instance plugin documentation paths + * Declares localized metadata paths, custom theme configurations, multi-instance plugin documentation paths * (Standard Docs + Cookbooks), custom localized sitemap filtration schemes, and third-party tracking scripts. - * + * * @environment Node.js (Build-time compilation script) * @see {@link https://docusaurus.io/docs/api/docusaurus-config | Docusaurus Configuration API Documentation} */ @@ -12,90 +12,104 @@ import { themes as prismThemes } from "prism-react-renderer"; /** * Global configuration data schema structure for Docusaurus system operations. - * + * * @type {import('@docusaurus/types').Config} - * @property {string} title - Primary core website branding text headline. - * @property {string} tagline - SEO and card fallback metadata summary description block. - * @property {Object} future - Flag registry optimizing compatibility properties with modern up-stream tools. - * @property {Object} customFields - Storage object injecting custom corporate legal copyright labels into the runtime environment. - * @property {Array>} presets - Classic Docusaurus preset bundle setups handling theme layouts and core document paths. - * @property {Array>} plugins - Custom multi-instance document routes and isolated post processors parsing recent content folders. - * @property {import('@docusaurus/types').ThemeConfig} themeConfig - The master styling architecture setting default light-modes, banners, nav bars, and HTML social links. */ + +// πŸ”” ANNOUNCEMENT BANNER TOGGLE: Set this to true to turn on an alert bar at the top of every page. +const SHOW_ANNOUNCEMENT = false; + const config = { + // --- CORE WEBSITE IDENTITY --- title: "The Scouting Units of American Legion Post 331", tagline: - "Scouting America Units Troop 303, Troop 331, Crew 303, and Pack 303 of Brownsburg, Indiana", + "Discover character, leadership, and outdoor adventure for youth ages 5-20 with the Brownsburg, IN Scouting America units at Post 331.", favicon: "img/logos/favicon.png", - // Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future + // Future flags ensure our code remains compatible with upcoming major versions of Docusaurus. + // See https://docusaurus.io#future future: { v4: true, // Improve compatibility with the upcoming Docusaurus v4 }, - // Set the production url of your site here + // The live public web address where parents and the community access the site. url: "https://brownsburgscouts.org", - baseUrl: "/", + baseUrl: "/", // Tells the server that the website is installed at the root directory level - onBrokenLinks: "throw", - onBrokenAnchors: "ignore", + // Guardrails to prevent broken links from going live. + onBrokenLinks: "throw", // ❌ STOPS the build process immediately if a Scout links to a page or image that does not exist. + onBrokenAnchors: "ignore", // 🟑 Ignores minor section-header anchor tag mistakes so they don't break the build pipeline. + // Internationalization settings (Language control). i18n: { defaultLocale: "en", locales: ["en"], }, - trailingSlash: false, + trailingSlash: false, // Ensures consistent URL structures across the site for better search visibility. + // Reusable custom text values that can be dropped into page footers or layouts dynamically. customFields: { copyright1: `Β© ${new Date().getFullYear()} The Scouting Units of American Legion Post 331, Scouting America`, copyright2: `All Rights Reserved`, }, + // --- CORE WEBSITE PRESETS --- presets: [ [ "classic", { + // We set core blog/docs to false here because we manage them manually below using multi-instance settings. blog: false, docs: false, theme: { - customCss: "./src/css/custom.css", - }, - sitemap: { - lastmod: "date", - changefreq: "weekly", - priority: 0.5, - ignorePatterns: ["/tags/**"], - filename: "sitemap.xml", - createSitemapItems: async (params) => { - const { defaultCreateSitemapItems, ...rest } = params; - const items = await defaultCreateSitemapItems(rest); - return items.filter((item) => !item.url.includes("/page/")); - }, + customCss: "./src/css/custom.css", // The central styling file for changing fonts and brand colors. }, + // --- SEARCH ENGINE SITEMAP ENGINE --- + // Dynamically turns off sitemap generation during Cloudflare Preview builds to save build time for the Scouts. + sitemap: + process.env.SKIP_SITEMAP == "true" + ? false + : { + lastmod: "date", + changefreq: "weekly", + priority: 0.5, + ignorePatterns: ["/tags/**"], // Bypasses internal organization tags to keep search results clean. + filename: "sitemap.xml", + // Custom filter that strips out pagination pages (like /page/2) from search engine results. + createSitemapItems: async (params) => { + const { defaultCreateSitemapItems, ...rest } = params; + const items = await defaultCreateSitemapItems(rest); + return items.filter((item) => !item.url.includes("/page/")); + }, + }, }, ], ], + // --- CUSTOM WEBSITE PLUGINS & EXTENSIONS --- plugins: [ + // πŸ“– MULTI-INSTANCE DOCS #1: The Camping Cookbook section. [ "@docusaurus/plugin-content-docs", { id: "cookbook", - path: "cookbook", - routeBasePath: "cookbook", - sidebarPath: "./sidebarCookbook.js", + path: "cookbook", // Looks for folder named 'cookbook' in the root of the project. + routeBasePath: "cookbook", // Makes the website URL point to brownsburgscouts.org/cookbook. + sidebarPath: "./sidebarCookbook.js", // The control panel file managing the cookbook's left-hand menu tree. }, ], + // πŸ“– MULTI-INSTANCE DOCS #2: Core unit documents and shared files. [ "@docusaurus/plugin-content-docs", { id: "docs", - path: "docs", - routeBasePath: "docs", - sidebarPath: "./sidebarDocs.js", + path: "docs", // Looks for folder named 'docs' in the root of the project. + routeBasePath: "docs", // Makes the website URL point to brownsburgscouts.org/docs. + sidebarPath: "./sidebarDocs.js", // The control panel file managing the core documentation menu tree. }, ], + // ✍️ CUSTOM EXTENSION: Automated recent adventure post processor. [ "./plugins/recent-blog-posts", { @@ -106,30 +120,70 @@ const config = { onInlineTags: "warn", onInlineAuthors: "warn", onUntruncatedBlogPosts: "warn", - } + // πŸ’‘ SAFETY OVERRIDE: Automatically assigns a default Scouting logo if a Scout forgets to add an author profile photo. + processBlogPosts: async ({ blogPosts }) => { + const DEFAULT_IMAGE = "/img/logos/favicon.png"; + + return blogPosts.map((post) => { + if (post.metadata && post.metadata.authors) { + post.metadata.authors = post.metadata.authors.map((author) => ({ + ...author, + imageURL: author.imageURL || DEFAULT_IMAGE, // Fallback safety catch + })); + } + return post; + }); + }, + }, + ], + // πŸͺ PRIVACY COMPLIANCE: Optional cookie consent pop-up banner. + // Set 'enabled: true' if you decide to activate tracking analytics in the future. + [ + "docusaurus-plugin-cookie-consent", + { + title: "Cookie Consent", + description: + "We use cookies to enhance your browsing experience and analyze our traffic.", + links: [ + { label: "Privacy Policy", href: "/privacy" }, + { label: "Cookie Policy", href: "/cookies" }, + ], + enabled: false, + acceptAllText: "Accept All Cookies", + rejectOptionalText: "Essential Only", + rejectAllText: "Reject All", + toastMode: true, + }, ], ], + // --- VISUAL THEME ARCHITECTURE & UI LAYOUTS --- themeConfig: { - image: "img/logos/favicon.png", + image: "img/logos/favicon.png", // Default image used when links are shared on text messages or social cards. colorMode: { respectPrefersColorScheme: true, - disableSwitch: true, + disableSwitch: true, // Forces light mode across the site to guarantee crisp visibility of unit layouts. defaultMode: "light", }, - // announcementBar: { - // id: "new_website", - // content: "Welcome to our new website! Please poke around and if something could be improved, contact the webmaster.", - // backgroundColor: "var(--announcement-bar)", - // textColor: "var(--scouting-america-white)", - // isCloseable: true, - // }, + // Configures the header banner alert when active. Controlled by the SHOW_ANNOUNCEMENT toggle at the top of this file. + announcementBar: SHOW_ANNOUNCEMENT + ? { + id: "announcement-bar", + content: "This is an announcement", + backgroundColor: "var(--announcement-bar)", // Links to a color variable set in src/css/custom.css + textColor: "var(--scouting-america-white)", + isCloseable: true, + } + : undefined, + + // --- NAVIGATION BAR CONFIGURATION --- navbar: { title: "Scouting America", logo: { alt: "Scouting America Units", src: "img/logos/all-units-logo.png", }, + // Left and right aligned items sitting at the top of the webpage. items: [ { type: "dropdown", @@ -151,20 +205,22 @@ const config = { to: "/join-us", label: "Join Us", position: "right", - className: "button button--secondary", + className: "button button--secondary", // Applies a standalone decorative theme button styling. }, ], - hideOnScroll: false, + hideOnScroll: false, // Keeps navigation links immediately accessible at the top while reading down pages. }, + + // --- FOOTER SECTION --- footer: { - style: "dark", + style: "dark", // Employs the charcoal/black theme layout block at the bottom of the page. links: [ { title: "Quick Links", items: [ { label: "Documents", - to: "/docs/general-docs", + to: "/docs/general", }, { label: "Blog", @@ -172,7 +228,7 @@ const config = { }, { label: "Helpful Links", - to: "/docs/general-docs/helpful-links" + to: "/docs/general/helpful-links", }, ], }, @@ -188,7 +244,7 @@ const config = { YouTube Channel - ` + `, }, { html: ` @@ -199,7 +255,7 @@ const config = { Troop 303 - ` + `, }, { html: ` @@ -210,7 +266,7 @@ const config = { Troop 331 - ` + `, }, { html: ` @@ -221,7 +277,7 @@ const config = { Pack 303 - ` + `, }, { html: ` @@ -232,10 +288,11 @@ const config = { Crew 303 - ` + `, }, ], }, + // --- CONTACT US FOOTER COLUMN --- { title: "Contact Us", items: [ @@ -249,47 +306,58 @@ const config = { Brownsburg, IN 46112 - ` + `, }, { html: ` +
Email Us
- ` + `, }, ], }, ], }, + // --- CODE BLOCKS SYNTAX HIGHLIGHTING --- + // Controls how programming snippets look when displayed in documentation tutorials or cookbook instructions. prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, + theme: prismThemes.github, // Uses clean light colors matching general GitHub documentation layouts. + darkTheme: prismThemes.dracula, // Fallback dark color block theme format. }, + // --- MERMAID DIAGRAM OPERATOR --- + // Configures flowchart layout trees so we can build unit organization maps using text commands. mermaid: { options: { - securityLevel: "loose", + securityLevel: "loose", // Necessary to allow custom CSS styling tags to color our flow charts properly. }, }, + // --- GLOBAL SEO GOOGLE KEYWORDS --- + // These hidden search tokens help parents in Brownsburg, Indiana find our Scouting units when searching on Google. metadata: [ { name: "keywords", content: - "eagle scout, webelos, scouts bsa, boy scouts near me, sea scouts, scoutbook, Venture, bsa, Boy Scouts of America, cub scouts, scouts, kids events near me, kid friendly activities near me, fun places for kids near me, scout, boy scouts, Scouting America, Things to do with kids near me, Kids activities near me, kids activities, child development, kids fun near me, trails near me, crafts for kids, Tent camping near me, science experiments for kids, science projects for kids, stem for kids, Canoe, trails near me, hiking trails near me, all trails, campsites, walking trails near me, Camping, Campground, Hiking near me, Camping near me, campgrounds near me, hiking trails near me, Fishing, Swimming, Brownsburg scout troops, Brownsburg kids, find cub scouts near me, find boy scouts near me, find girl scouts near me", + "scouts bsa brownsburg, cub scouts near me, brownsburg scout troops, troop 303 brownsburg, troop 331 indiana, pack 303 indiana, crew 303, scouting america indiana, boy scouts brownsburg indiana, girl scouts bsa hendricks county, youth groups brownsburg in, kids activities brownsburg indiana, kid friendly clubs near me, youth leadership programs, eagle scout rank, cub scout advancement, kids outdoor activities hendricks county, family camping brownsburg, stem activities for kids indiana, youth community service brownsburg, child development groups, scouts bsa girls troop, cub scouts avon indiana, boy scouts pittsboro in, youth sports and adventure brownsburg, child character building programs, community youth organizations indiana", }, ], }, - themes: ["@docusaurus/theme-mermaid"], + + // --- MARKDOWN & PARSING ENGINES --- + themes: ["@docusaurus/theme-mermaid"], // Extends theme engine capabilities to natively render Mermaid charts. markdown: { - format: "mdx", - mermaid: true, - emoji: true, + format: "mdx", // Enforces rich MDX format so we can embed custom interactive buttons inside text files. + mermaid: true, // Turns on graph generation tools within standard markdown documents. + emoji: true, // Allows Scouts to write basic shortcuts like :tent: or :fire: to automatically show visual emojis. + + // --- SAFETY HOOKS & COMPILATION GUARDRAILS --- hooks: { - onBrokenMarkdownLinks: "warn", - onBrokenMarkdownImages: "throw", + onBrokenMarkdownLinks: "warn", // 🟑 Warns us in the terminal if a text link points to an invalid section header anchor. + onBrokenMarkdownImages: "throw", // ❌ CRASHES the local builder instantly if a Scout tries to link a photo that is missing. }, }, }; diff --git a/note.md b/note.md new file mode 100644 index 0000000..532e402 --- /dev/null +++ b/note.md @@ -0,0 +1,130 @@ +To connect your automated pipeline to Facebook, you need to configure 8 distinct +Repository Secrets in your GitHub repository settings. The process requires +creating an app on the Meta for Developers portal, assigning permissions, +generating system user tokens that never expire, and saving those keys into +GitHub. [1, 2] + +--- + +## Step 1: The Repository Secrets Checklist + +You must add these exact pairs of names to your GitHub Secrets inventory: + +| Scouting Unit | Page ID Secret Name | Access Token Secret Name | +| -------------- | -------------------- | ---------------------------------------------------- | +| Troop 303 | FB_PAGE_ID_TROOP_303 | FB_ACCESS_TOKEN_TROOP_303 | +| Troop 331 | FB_PAGE_ID_TROOP_331 | FB_ACCESS_TOKEN_TROOP_331 | +| Pack 303 | FB_PAGE_ID_PACK_303 | FB_ACCESS_TOKEN_PACK_303 | +| Crew 303 | FB_PAGE_ID_CREW_303 | FB_ACCESS_TOKEN_CREW_303 | +| Fallback / All | FB_PAGE_ID_DEFAULT | FB_ACCESS_TOKEN_DEFAULT (Point to your primary page) | + +--- + +## Step 2: How to Find Your Facebook Page IDs + +Finding your Page IDs is simple and does not require developer tools: + +1. Log into Facebook and switch your profile to the target page (e.g., Troop + 303). +2. Go to the page's profile view and look at the URL in your browser address + bar. The long number at the very end of the URL is your Page ID (e.g., + https://facebook.com...). +3. Alternatively, click the About tab on your page, scroll down to Page + Transparency, and copy the number listed there. [3] + +--- + +## Step 3: Create a System App on Meta for Developers + +Standard developer access tokens expire after 60 days. To generate permanent +tokens that never time out, you must create a Meta App: + +1. Navigate to [facebook.com](https://developers.facebook.com/) and log in with + your primary Facebook account. +2. Click My Apps in the top right, then click Create App. +3. Select Other as the app use case, click Next, and choose Business as your + app type. +4. Give your app a name (e.g., Scout Site Automation), select your Business + Account from the dropdown menu, and click Create App. [4, 5, 6, 7] + +--- + +## Step 4: Generate Permanent System User Access Tokens + +1. From your Meta App dashboard, look at the sidebar and click Tools β†’ Business + Settings. This opens your Meta Business Suite dashboard. +2. In the Business Settings sidebar, click Users β†’ System Users. [8, 9] +3. Click Add to provision a new system user. Name it GitHub Actions Router and + set the role to Admin. +4. Select your newly created System User, click Add Assets, choose Pages, + select all your unit pages, and toggle on Full Control (Everything). Save + your changes. [10] +5. With the System User still highlighted, click the Generate New Token button. +6. Select your App from the dropdown list, and check these exact permission + scopes: + +- pages_manage_posts + - pages_read_engagement + - pages_show_list + +7. Click Generate Token. Copy this string immediately. This is your permanent + Page Access Token. Repeat this generation step for each unique page asset if + your pages are held across different business accounts. [11, 12] + +--- + +## Step 5: Save the Values in GitHub Secrets + +Once you have gathered all your Page IDs and token strings, encrypt them within +your repository configuration panel: + +1. Open your web browser and go to your target GitHub Repository. +2. Click the Settings tab (the gear icon at the top of the interface menu). +3. Scroll down the left sidebar to the Security header block, click Secrets and + variables, and select Actions. +4. Click the green New repository secret button. +5. In the Name input field, type the exact variable name (e.g., + FB_ACCESS_TOKEN_TROOP_303). +6. Paste the corresponding token string value into the large Secret text area + box. +7. Click Add secret to encrypt and lock down the key. Repeat this process until + all 8 variable properties match your target configurations. [13, 14, 15, 16, + 17] + +If your script triggers but throws a "Permission Denied" log error during +testing, I can show you how to use the Meta Access Token Debugger tool to verify +your token parameters. Would you like me to map out those validation steps? + +[1] +[https://gitprotect.io](https://gitprotect.io/blog/how-to-safely-store-secrets-in-github/) +[2] +[https://baserow.io](https://baserow.io/user-docs/configure-facebook-for-oauth-2-sso) +[3] +[https://www.socialmediaexaminer.com](https://www.socialmediaexaminer.com/4-ways-to-use-google-tag-manager-with-facebook/) +[4] +[https://www.digittrix.com](https://www.digittrix.com/scripts/facebook-login-implementation-in-react-and-nodejs) +[5] +[https://docs.n8n.io](https://docs.n8n.io/integrations/builtin/credentials/facebookapp/) +[6] +[https://www.facebook.com](https://www.facebook.com/MetaforDevelopers/videos/get-started-with-the-messenger-api-for-instagram/389112236490456/) +[7] +[https://ckmobile.medium.com](https://ckmobile.medium.com/nextauth-part-8-facebook-provider-2b058b0ae7bb) +[8] +[https://developers.facebook.com](https://developers.facebook.com/documentation/facebook-login/guides/access-tokens) +[9] +[https://fivetran.com](https://fivetran.com/docs/activations/destinations/available-destinations/facebook-ads) +[10] +[https://gist.github.com](https://gist.github.com/michaelkarrer81/88fbc36d99a8a32a83f3efe234f7690a) +[11] +[https://help.zscaler.com](https://help.zscaler.com/uvm/configuring-github-advanced-security-connector) +[12] +[https://mixedanalytics.com](https://mixedanalytics.com/knowledge-base/import-facebook-page-data-to-google-sheets/) +[13] +[https://www.linkedin.com](https://www.linkedin.com/pulse/git-github-punit-dhiman-wouse) +[14] +[https://chrisjhart.com](https://chrisjhart.com/Creating-A-Simple-Free-Blog-Hugo/) +[15] [https://github.com](https://github.com/orgs/community/discussions/170965) +[16] +[https://www.storylane.io](https://www.storylane.io/tutorials/how-to-add-secrets-to-github) +[17] +[https://www.linkedin.com](https://www.linkedin.com/learning/data-pipeline-automation-with-github-actions-using-r-and-python/setting-secrets-and-environment-variables) diff --git a/package-lock.json b/package-lock.json index 3c6f1c2..ffbe985 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "csv-parse": "^6.2.1", + "docusaurus-plugin-cookie-consent": "^4.6.0", "leaflet": "^1.9.4", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", @@ -11889,6 +11890,18 @@ "node": ">=6" } }, + "node_modules/docusaurus-plugin-cookie-consent": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-cookie-consent/-/docusaurus-plugin-cookie-consent-4.6.0.tgz", + "integrity": "sha512-eSFlW0PRODcLtrkkXujbkgt4dIGQWPsVyfIs3O09+Q1KzV7EyPVowec1vwROa7opJwYnON/yEf/czdP1D3XM/Q==", + "license": "MIT", + "peerDependencies": { + "@docusaurus/core": "^3.0.0", + "@docusaurus/types": "^3.0.0", + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", diff --git a/package.json b/package.json index f25284d..e827645 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "csv-parse": "^6.2.1", + "docusaurus-plugin-cookie-consent": "^4.6.0", "leaflet": "^1.9.4", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", diff --git a/plugins/recent-blog-posts.js b/plugins/recent-blog-posts.js index 719214e..4b97d87 100644 --- a/plugins/recent-blog-posts.js +++ b/plugins/recent-blog-posts.js @@ -2,7 +2,7 @@ * @file recent-blog-posts.js * @description A custom local Docusaurus plugin decorator that extends the core blog plugin. * Intercepts the build-time data lifecycle hook (`contentLoaded`), filters out unlisted posts, - * truncates the list to the 5 most recent records, and flushes them directly to a local JSON + * truncates the list to the 4 most recent records, and flushes them directly to a local JSON * schema file. This enables client-side components to safely load recent blog metadata without * bundling massive layout trees. * @@ -26,6 +26,7 @@ const defaultBlogPlugin = blogPluginExports.default; */ async function blogPluginEnhanced(...pluginArgs) { const blogPluginInstance = await defaultBlogPlugin(...pluginArgs); + // This is the hidden background folder where Docusaurus builds temporary files const dir = ".docusaurus"; return { @@ -43,25 +44,33 @@ async function blogPluginEnhanced(...pluginArgs) { * @returns {Promise} Resolves when downstream base core operations complete execution. */ contentLoaded: async function (data) { + // Step 1: Create a safe copy of all existing blog posts let recentPosts = [...data.content.blogPosts] - // Only show published posts. + // Step 2: Remove any posts marked as hidden or unlisted .filter((p) => !p.metadata.unlisted) + // Step 3: Cut the list down to only keep the 4 most recent adventures .slice(0, 4); + // Step 4: Clean up the data layout to keep the file size incredibly tiny recentPosts = recentPosts.map((p) => { return { id: p.id, metadata: { + // Safely import title, date, permalink, description, tags, and processed author arrays ...p.metadata, }, }; }); + // Step 5: Make sure the hidden tracking folder exists so the computer doesn't crash fs.mkdirSync(dir, { - recursive: true, // Avoid error if directory already exists. + recursive: true, // If the folder already exists, safely skip creating a new one }); - fs.writeFileSync(`${dir}/recent-posts.json`, JSON.stringify(recentPosts)); + // Step 6: Convert the post list into a plain-text file so front-end widgets can load it quickly + fs.writeFileSync(`${dir}/recent-posts.json`, JSON.stringify(recentPosts, null, 2)); + + // Step 7: Tell Docusaurus to finish setting up the rest of the website normally return blogPluginInstance.contentLoaded(data); }, }; diff --git a/sidebarDocs.js b/sidebarDocs.js index 416d9ee..482fcec 100644 --- a/sidebarDocs.js +++ b/sidebarDocs.js @@ -15,23 +15,30 @@ * @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ export default { + // Defines the individual sidebar identifier name referenced inside docusaurus.config.js sidebarDocs: [ + // --------------------------------------------------------------------- + // CATEGORY 1: General cross-unit documentation mapping block + // --------------------------------------------------------------------- { - type: 'category', - label: 'General Documents', + type: 'category', // Renders an expandable multi-item folder dropdown block + label: 'General Documents', // Public text header label displayed inside the UI sidebar link: { - type: 'generated-index', - title: 'General Documents', - description: 'General documents applicable to multiple units', - slug: '/general-docs' + type: 'generated-index', // Instructs Docusaurus to auto-generate a landing dashboard view page + title: 'General Documents', // Headline title text used inside the auto-generated dashboard view + description: 'General documents applicable to multiple units', // Explanatory subtitle string + slug: '/general' // Base web URL route path mapping address for this index node }, items: [ { - type: 'autogenerated', - dirName: 'general-docs' + type: 'autogenerated', // Enables automatic scanning of your local file directory trees + dirName: 'general' // Recursively imports files located inside the "docs/general/" path } ] }, + // --------------------------------------------------------------------- + // CATEGORY 2: Crew 303 documentation folder block + // --------------------------------------------------------------------- { type: 'category', label: 'Crew 303', @@ -44,10 +51,13 @@ export default { items: [ { type: 'autogenerated', - dirName: 'crew-303' + dirName: 'crew-303' // Recursively imports files located inside the "docs/crew-303/" path } ] }, + // --------------------------------------------------------------------- + // CATEGORY 3: Pack 303 documentation folder block + // --------------------------------------------------------------------- { type: 'category', label: 'Pack 303', @@ -60,10 +70,13 @@ export default { items: [ { type: 'autogenerated', - dirName: 'pack-303' + dirName: 'pack-303' // Recursively imports files located inside the "docs/pack-303/" path } ] }, + // --------------------------------------------------------------------- + // CATEGORY 4: Troop 303 documentation folder block + // --------------------------------------------------------------------- { type: 'category', label: 'Troop 303', @@ -76,10 +89,13 @@ export default { items: [ { type: 'autogenerated', - dirName: 'troop-303' + dirName: 'troop-303' // Recursively imports files located inside the "docs/troop-303/" path } ] }, + // --------------------------------------------------------------------- + // CATEGORY 5: Troop 331 documentation folder block + // --------------------------------------------------------------------- { type: 'category', label: 'Troop 331', @@ -92,9 +108,9 @@ export default { items: [ { type: 'autogenerated', - dirName: 'troop-331' + dirName: 'troop-331' // Recursively imports files located inside the "docs/troop-331/" path } ] } ] -} \ No newline at end of file +} diff --git a/src/components/BlogCard/index.jsx b/src/components/BlogCard/index.jsx index cb786ad..fff6844 100644 --- a/src/components/BlogCard/index.jsx +++ b/src/components/BlogCard/index.jsx @@ -14,22 +14,27 @@ */ import React from "react"; -import Link from '@docusaurus/Link'; -import clsx from "clsx"; -import styles from './styles.module.css'; -import Heading from '@theme/Heading'; -import recentPosts from '@site/.docusaurus/recent-posts.json'; -import useBaseUrl from "@docusaurus/useBaseUrl"; +import Link from '@docusaurus/Link'; // Docusaurus optimized router link component to prevent full-page reloads +import clsx from "clsx"; // Utility for conditionally joining CSS class names together cleanly +import styles from './styles.module.css'; // Scoped CSS Modules styling sheet for this specific layout component +import Heading from '@theme/Heading'; // Swappable theme heading component supporting semantic HTML structures +import recentPosts from '@site/.docusaurus/recent-posts.json'; // Pre-built local JSON database payload containing recent post metadata +import useBaseUrl from "@docusaurus/useBaseUrl"; // Appends the site's configured baseUrl configuration prefix to static paths /** - * Localizes an ISO date string to "MMM DD, YYYY" format (e.g., "Oct 24, 2026"). + * Localizes an ISO date string to "MMM DD, YYYY" using UTC to prevent timezone offsets. * @private * @param {string} isoString - The ISO date format string from Docusaurus metadata. * @returns {string} The formatted date. */ const formatDate = (isoString) => { - const date = new Date(isoString); - return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(date); + const date = new Date(isoString); // Instantiates a native JavaScript Date engine object + return new Intl.DateTimeFormat('en-US', { // Leverages ECMAScript Internationalization API for lightweight date building + month: 'short', // Formats month into a 3-letter shorthand descriptor (e.g., "Jan") + day: 'numeric', // Formats day into standard integer characters (e.g., "18") + year: 'numeric', // Formats year into a 4-digit layout block (e.g., "2026") + timeZone: 'UTC' // Enforces universal time to guard against localized browser skew shifts + }).format(date); // Commits parsing transformations to output a finalized string wrapper }; /** @@ -44,24 +49,39 @@ const formatDate = (isoString) => { * @param {Object} props.frontmatter - Unprocessed raw frontmatter fields containing configuration like custom cover images. */ function BlogCard({ permalink, title, date, authors, tags }) { + // Pre-caches the global server asset path prefixing mapping rule for the default background const fallbackDefaultImage = useBaseUrl('img/blog/default-blog-cover.webp'); - // 1. Strip trailing slashes and get the folder name from permalink - const cleanPath = permalink.replace(/^\/|\/$/g, ''); + // 1. Isolate the base slug name by stripping the leading "/blog" and trailing slashes + const cleanUrl = permalink.replace(/^\/|\/$/g, '').replace(/^blog\//, ''); + + // 2. Isolate ONLY the final trailing title string + const slugName = cleanUrl.split('/').pop(); + + // 3. Extract the exact YYYY-MM-DD prefix from the raw ISO string directly without date manipulation + // An ISO timestamp starts with "YYYY-MM-DD", so we slice the first 10 characters + const datePrefix = date.slice(0, 10); + + // 4. Combine them into the exact format requested: YYYY-MM-DD-blogtitle + const folderName = `${datePrefix}-${slugName}`; let resolvedCoverUrl; try { // 2. Webpack looks inside the static folder during compile time - resolvedCoverUrl = require(`@site/static/img/${cleanPath}/cover.webp`).default; + // Dynamically checks for the presence of an optimized webp illustration file asset block at compilation time + resolvedCoverUrl = require(`@site/static/img/blog/${folderName}/cover.webp`).default; } catch (err) { // 3. If file doesn't exist, it instantly uses the fallback at build time + // Fallback error-handling catching missing directory trees to seamlessly inject standard cards instead resolvedCoverUrl = fallbackDefaultImage; } return ( + // Infuses standard Infima CSS grid infrastructure properties (allocating 3 out of 12 columns per entry)
+ {/* 1. Card Image Header Frame Container */}
- {/* 2. Card Body */} + {/* 2. Card Body Content Presentation Wrapper */}
+ {/* Converts structural timestamps into beautiful localized textual outputs */} {formatDate(date)}

{title}

+ {/* Iterates through a limited section slice of taxonomies to layout classification labels */}
{tags.slice(0, 3).map((tag, idx) => ( @@ -83,23 +105,26 @@ function BlogCard({ permalink, title, date, authors, tags }) { ))}
- {/* 3. Footer: Authors & Read More */} + {/* 3. Footer: Authors Avatar Profiles Stack & Direct Action Link */}
+ {/* Loops over the individual post authors block data mapping variables */} {authors.map((author, idx) => ( {author.name} ))} + {/* Dynamic summary phrase adjustment matching plural criteria boundaries */} {authors.length === 1 ? authors[0].name : `${authors.length} Authors`}
+ {/* Visual element anchor pointing to the comprehensive blog post review content view */} Read →
@@ -108,27 +133,35 @@ function BlogCard({ permalink, title, date, authors, tags }) { ); } +/** + * Root component that maps out the collective layout grid workspace dashboard view. + * @public + * @returns {JSX.Element} Structural framework rendering recent post items. + */ export default function HomepageBlogCards() { return ( <> + {/* Layout container aligning content cleanly along central layout coordinate nodes */}
Recent Adventures
+ {/* Standard center-aligned responsive Infima CSS grid structural layout row box wrapper */}
+ {/* Dynamically steps down through the collection items data payload to inject the components grid */} {recentPosts.map((post) => ( ))}
); -} \ No newline at end of file +} diff --git a/src/components/BlogCard/styles.module.css b/src/components/BlogCard/styles.module.css index c1636b8..ddfa475 100644 --- a/src/components/BlogCard/styles.module.css +++ b/src/components/BlogCard/styles.module.css @@ -1 +1,175 @@ -.blogCard{display:flex;flex-direction:column;height:100%;background:var(--ifm-card-background-color);border:1px solid var(--ifm-color-emphasis-200);border-radius:12px;overflow:hidden;transition:transform .2s,box-shadow .2s,border-color .2s;text-decoration:none!important;color:inherit}.cardMeta,.date{display:flex;margin-bottom:10px;font-size:.85rem;color:var(--ifm-color-emphasis-600)}.blogCard:hover{transform:translateY(-5px);box-shadow:var(--global-box-shadow);border-color:var(--ifm-color-primary)}.cardHeader{height:160px;overflow:hidden}.cardImage{width:100%;height:100%;object-fit:cover;transition:transform .5s}.blogCard:hover .cardImage{transform:scale(1.05)}.cardBody{padding:1.5rem;display:flex;flex-direction:column;flex-grow:1}.date{justify-content:space-between;align-items:center}.cardMeta{gap:8px;align-items:center}.tagPill{background-color:var(--ifm-color-primary-lightest);color:var(--ifm-color-primary-dark);padding:2px 8px;border-radius:12px;font-size:.75rem;font-weight:700}.cardTitle{font-size:1.25rem;font-weight:700;margin-bottom:.5rem;line-height:1.3}.cardDescription{font-size:.9rem;color:var(--ifm-color-emphasis-700);margin-bottom:1.5rem;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.cardFooter{margin-top:auto;display:flex;align-items:center;justify-content:space-between;padding-top:1rem;border-top:1px solid var(--ifm-color-emphasis-200)}.authorStack{display:flex;align-items:center}.authorAvatar{width:28px;height:28px;border-radius:50%;border:2px solid var(--ifm-card-background-color);margin-right:-8px;object-fit:cover}.authorAvatar:last-of-type{margin-right:8px}.authorName{font-size:.85rem;font-weight:600;margin-left:5px}.readMore{color:var(--ifm-color-primary);font-weight:700;font-size:.9rem}.newBadge{position:absolute;top:12px;right:12px;background-color:var(--ifm-color-primary);color:var(--ifm-color-white);padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:700;letter-spacing:1px;z-index:10;box-shadow:var(--global-box-shadow);pointer-events:none;animation:2s infinite pulse}@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}} \ No newline at end of file +/* ============================================================================== + CSS Module Stylesheet: Blog Post Display Grid Cards + Structures responsive grid blocks integrating with Docusaurus Infima variables. + ============================================================================== */ + +/* Main overarching container component card link frame wrapper */ +.blogCard { + display: flex; + flex-direction: column; /* Arranges child items inside a vertical top-to-bottom stack layout */ + height: 100%; /* Spans full height of grid cell blocks ensuring equal rows layout grid matching */ + background: var(--ifm-card-background-color); /* Extracts Docusaurus central light/dark UI design mode layout backdrop colors */ + border: 1px solid var(--ifm-color-emphasis-200); /* Thin subtle bounding frame line outline */ + border-radius: 12px; /* Soft modern curved structural card corner geometry edges */ + overflow: hidden; /* Clips inside assets like header photos so they do not bleed past the corner curve framework */ + transition: transform .2s, box-shadow .2s, border-color .2s; /* Clean hardware-accelerated transformation frame timings */ + text-decoration: none !important; /* Suppresses native browser text underline styling decorations across content child blocks */ + color: inherit; /* Absorbs upstream site font palette color configurations */ +} + +/* Collective mapping grouping mutual visual metadata font sizing attributes together */ +.cardMeta, +.date { + display: flex; + margin-bottom: 10px; + font-size: .85rem; /* Clean readable text typography size downscale properties */ + color: var(--ifm-color-emphasis-600); /* Secondary accent text gray typography palette rule */ +} + +/* Hover tracking transition rules modifying layout variables when a cursor glides over the card */ +.blogCard:hover { + transform: translateY(-5px); /* Slightly lifts the card upward along the vertical viewport coordinate mapping nodes */ + box-shadow: var(--global-box-shadow); /* Projects deep backdrop shadow values for three-dimensional visual depth look */ + border-color: var(--ifm-color-primary); /* Switches border strokes to match the core branding primary color scheme configuration */ +} + +/* Framing boundary cell holding the background blog post illustration cover image asset */ +.cardHeader { + height: 160px; /* Fast locked boundary height line limiting initial graphic dimension allocations */ + overflow: hidden; /* Conceals structural scaling animations extending past outer boundaries */ +} + +/* Core background cover image presentation property class layout values */ +.cardImage { + width: 100%; + height: 100%; + object-fit: cover; /* Scales and centers illustration frames without warping aspect ratio parameters */ + transition: transform .5s; /* Smooth, slow duration rule handling image growth zoom calculations */ +} + +/* Cinematic zoom animation rule scaling the header illustration inward when user handles hover items */ +.blogCard:hover .cardImage { + transform: scale(1.05); /* Extends file image asset tracking dimensions outwards by exactly 5% scale margins */ +} + +/* Inner content typography compartment box boundary container */ +.cardBody { + padding: 1.5rem; /* Generates equal, deep margins formatting readable inside cell spacing padding bounds */ + display: flex; + flex-direction: column; + flex-grow: 1; /* Forces internal cell block spaces to inflate outward to fill lingering empty footer fields */ +} + +/* Arranges content structures horizontally across endpoints inside the date tracking row block */ +.date { + justify-content: space-between; /* Separates child parameters uniformly across opposite ends of the container */ + align-items: center; /* Locks target items along centralized vertical coordinate balancing nodes */ +} + +/* Taxonomy tags row structural configuration layout wrapper */ +.cardMeta { + gap: 8px; /* Enforces static gaps separating neighboring category items without margins overrides */ + align-items: center; +} + +/* Customized visual badge pills displaying classification tags (e.g., Troop 303) */ +.tagPill { + background-color: var(--ifm-color-primary-lightest); /* Light branding tone backdrop fill */ + color: var(--ifm-color-primary-dark); /* Dark contrasting text color scheme ensuring accessibility visibility rules */ + padding: 2px 8px; /* Compact spacing frames enclosing text values */ + border-radius: 12px; /* Capsule layout rounded side curves styling profile */ + font-size: .75rem; + font-weight: 700; /* Strong dense text formatting parameters */ +} + +/* Article headline title text layout configuration parameters */ +.cardTitle { + font-size: 1.25rem; + font-weight: 700; + margin-bottom: .5rem; + line-height: 1.3; /* Tightens spatial line stacks preventing multi-line headers from drifting apart */ +} + +/* Summary paragraph excerpt text box formatting framework boundaries */ +.cardDescription { + font-size: .9rem; + color: var(--ifm-color-emphasis-700); + margin-bottom: 1.5rem; + display: -webkit-box; /* Invokes legacy multi-line truncation support properties across Webkit rendering frameworks */ + -webkit-line-clamp: 3; /* Hard limits narrative paragraphs from extending past a maximum boundary count of 3 text rows */ + -webkit-box-orient: vertical; /* Dictates alignment truncation axis direction processing parameters */ + overflow: hidden; /* Truncates trailing overflows and appends native ellipsis points (...) automatically */ +} + +/* Bottom status panel component block anchoring authors metrics and read buttons */ +.cardFooter { + margin-top: auto; /* Leverages flex push tracking fields to pin footer arrays exactly to the card floor line */ + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 1rem; + border-top: 1px solid var(--ifm-color-emphasis-200); /* Separation accent line divider dividing body from foot contents */ +} + +/* Collates multiple profile faces closely together using overlapping configurations */ +.authorStack { + display: flex; + align-items: center; +} + +/* Multi-author portrait avatar rendering styling rules */ +.authorAvatar { + width: 28px; + height: 28px; + border-radius: 50%; /* Forces standard source square images to resolve as perfect circular layout graphics */ + border: 2px solid var(--ifm-card-background-color); /* Clean structural border matching card backing to mask overlaps cleanly */ + margin-right: -8px; /* Negative horizontal alignment margins shifting portraits leftwards into tight stack arrangements */ + object-fit: cover; +} + +/* Reset rules targeting terminal profile items inside the stacked face array layers */ +.authorAvatar:last-of-type { + margin-right: 8px; /* Negates negative margins for final elements to prevent profile name strings from overlapping faces */ +} + +/* Post creator description name details layout block text string elements */ +.authorName { + font-size: .85rem; + font-weight: 600; + margin-left: 5px; +} + +/* Read more action call text label link components */ +.readMore { + color: var(--ifm-color-primary); + font-weight: 700; + font-size: .9rem; +} + +/* Announcement tag badge overlaying the card workspace boundaries for urgent new items entries */ +.newBadge { + position: absolute; /* Breaks element away from baseline DOM document flows to place relative to coordinate points */ + top: 12px; /* Spacing offset position from card roof ceiling line */ + right: 12px; /* Spacing offset position from card right margin boundary wall */ + background-color: var(--ifm-color-primary); + color: var(--ifm-color-white); + padding: 4px 10px; + border-radius: 20px; + font-size: .7rem; + font-weight: 700; + letter-spacing: 1px; /* Extends characters outwards to boost overall visual tracking legibility features */ + z-index: 10; /* Forces tag to lift above baseline card contents to avoid image rendering occlusions */ + box-shadow: var(--global-box-shadow); + pointer-events: none; /* Disables click tracking intercept rules so click requests fall directly through to card link triggers */ + animation: 2s infinite pulse; /* Maps cyclical infinite engine loop running the custom scale transformation timing array */ +} + +/* Core keyframe math array configurations establishing tracking boundaries for pulsing notifications */ +@keyframes pulse { + 0%, 100% { + transform: scale(1); /* Neutral resting scale dimensions benchmark configuration */ + } + 50% { + transform: scale(1.05); /* Scales badge canvas layout slightly outwards by 5% at exactly the halfway runtime cycle node */ + } +} diff --git a/src/components/Column/index.jsx b/src/components/Column/index.jsx index b489ef7..3502d00 100644 --- a/src/components/Column/index.jsx +++ b/src/components/Column/index.jsx @@ -9,7 +9,7 @@ */ import React from "react"; -import clsx from "clsx"; +import clsx from "clsx"; // Utility engine for conditionally joining dynamic strings and CSS classes together /** * Renders a standard layout grid column container. @@ -23,8 +23,9 @@ import clsx from "clsx"; */ export default function Column({ children, className, style }) { return ( + // Combines the base structural 'col' styling rule with custom runtime classes passed via props
- {children} + {children} {/* Injects child components or text nodes into the rendered column framework */}
); } diff --git a/src/components/Columns/index.jsx b/src/components/Columns/index.jsx index f1b8ba8..c3e05f5 100644 --- a/src/components/Columns/index.jsx +++ b/src/components/Columns/index.jsx @@ -9,7 +9,7 @@ */ import React from "react"; -import clsx from "clsx"; +import clsx from "clsx"; // Utility engine used for conditionally joining dynamic string classes together /** * Renders a centered grid container and row wrapper for dynamic columns. @@ -23,10 +23,11 @@ import clsx from "clsx"; */ export default function Columns({ children, className, style }) { return ( - // This section encompasses the columns that we will integrate with children from a dedicated component to allow the addition of columns as needed + // Outer responsive grid framework wrapper centering the entire layout section block horizontally on the page canvas
+ {/* Creates the horizontal layout row, combining standard grid properties with dynamic custom styles and classes */}
- {children} + {children} {/* Injects and mounts nested child column components inside the row structure */}
); diff --git a/src/components/CsvTable/index.jsx b/src/components/CsvTable/index.jsx index f09558f..9a39327 100644 --- a/src/components/CsvTable/index.jsx +++ b/src/components/CsvTable/index.jsx @@ -17,15 +17,17 @@ import { parse } from 'csv-parse/browser/esm'; * @returns {JSX.Element} A loading block indicator, a red error notice message, or the completed data grid. */ export default function CsvTable({ csvUrl }) { - const [data, setData] = useState([]); - const [headers, setHeaders] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + // --- React State Hook Definitions --- + const [data, setData] = useState([]); // Allocation grid matrix holding array collection of parsed data row records + const [headers, setHeaders] = useState([]); // String array container mapping the extracted CSV table column headers + const [loading, setLoading] = useState(true); // Sets initial true visibility condition lock for async background operations + const [error, setError] = useState(null); // Null pointer block caching processing exceptions or networking errors useEffect(() => { // Execution Guard: Don't execute on the server side during Docusaurus build cycles if (typeof window === 'undefined') return; + // Asynchronous background function handling web asset retrieval pipelines async function fetchAndParseCsv() { try { // 1. Fetch the raw asset from the static folder @@ -33,7 +35,7 @@ export default function CsvTable({ csvUrl }) { if (!response.ok) { throw new Error(`HTTP network error! Status: ${response.status}`); } - const csvText = await response.text(); + const csvText = await response.text(); // Unpacks stream response payload into raw unformatted text layout strings // 2. Parse the text using the native ESM module configuration parse( @@ -43,9 +45,10 @@ export default function CsvTable({ csvUrl }) { skip_empty_lines: true, // Bypasses tailing blank spacing rows trim: true // Cuts accidental whitespaces off strings }, + // Callback execution routine fired when parsing processing ends (err, records) => { if (err) { - setError(err.message); + setError(err.message); // Intercepts parsing formatting discrepancies setLoading(false); return; } @@ -53,29 +56,32 @@ export default function CsvTable({ csvUrl }) { if (records && records.length > 0) { // Extract the column header keys from the first record object setHeaders(Object.keys(records[0])); - setData(records); + setData(records); // Stashes records array matrix within local React component hook storage } - setLoading(false); + setLoading(false); // Drops loading flag state to authorize grid layout visibility transitions } ); } catch (err) { - setError(err.message); + setError(err.message); // Intercepts network fetching anomalies setLoading(false); } } - fetchAndParseCsv(); - }, [csvUrl]); + fetchAndParseCsv(); // Triggers structural data retrieval function sequence + }, [csvUrl]); // Re-runs execution loop if target asset URL address maps to new coordinates + // --- Conditional UI Render Guards --- if (loading) return

⏳ Loading data table...

; if (error) return

❌ Error parsing data layout: {error}

; if (data.length === 0) return

⚠️ No data discovered inside the requested file mapping.

; return ( + // Infuses Docusaurus Infima markdown class configurations to style typography layouts beautifully
+ {/* Iterates through the isolated headers array to build table column header blocks */} {headers.map((header) => ( + {/* Iterates over the top-level array collection rows */} {data.map((row, rowIndex) => ( + {/* Nested iterator extracting individual key mapping values for every cell's cross point data field */} {headers.map((header) => ( ))} diff --git a/src/components/HeroCarousel/index.jsx b/src/components/HeroCarousel/index.jsx index 25e533e..1860c3b 100644 --- a/src/components/HeroCarousel/index.jsx +++ b/src/components/HeroCarousel/index.jsx @@ -13,13 +13,13 @@ */ import React from "react"; -import Slider from "react-slick"; -import clsx from "clsx"; -import Link from "@docusaurus/Link"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import Slider from "react-slick"; // Import the core react-slick sliding carousel framework layout component +import clsx from "clsx"; // Utility engine used for conditionally joining dynamic string classes together +import Link from "@docusaurus/Link"; // Docusaurus optimized router link component to prevent full-page reloads +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // React hook providing access to global site configuration variables -import Heading from "@theme/Heading"; -import styles from "./index.module.css"; +import Heading from "@theme/Heading"; // Swappable theme heading component supporting semantic HTML structures +import styles from "./index.module.css"; // Scoped CSS Modules styling sheet for this specific layout component /** * Renders the text overlay for the hero section, including the site title, @@ -30,14 +30,20 @@ import styles from "./index.module.css"; * @returns {React.JSX.Element} The branded content overlay block. */ function HeroText() { + // Destructures the siteConfig configuration payload from the central Docusaurus context provider const { siteConfig } = useDocusaurusContext(); return ( + // Inner typography container absolute-positioned over the active visual image layer
+ {/* Renders a semantic h1 block using the official site title from docusaurus.config.js */} {siteConfig.title} + {/* Renders the official site subtitle/tagline text from docusaurus.config.js */}

{siteConfig.tagline}

+ {/* Action button grouping container wrapper */}
+ {/* Large, high-contrast call-to-action button routing users directly to the recruitment form page */} Join Us @@ -54,30 +60,35 @@ function HeroText() { * @returns {React.JSX.Element} An autoplaying slideshow with interactive navigation overlays. */ export default function HeroCarousel() { + // Configuration options object passed to configure the underlying react-slick engine const settings = { - dots: true, - infinite: true, - fade: true, - speed: 1000, - slidesToShow: 1, - slidesToScroll: 1, - autoplay: true, - waitForAnimate: false, - arrows: false, + dots: true, // Enables the navigation dots tracker buttons at the bottom of the card frame + infinite: true, // Loops the carousel slides infinitely back to slide 1 upon reaching the terminal item + fade: true, // Deploys a smooth opacity cross-fade transition instead of a horizontal slide swipe action + speed: 1000, // Setting tracking animation cross-fade layout transition durations (1000ms = 1 second) + slidesToShow: 1, // Dictates the volume of slides exposed on screen simultaneously within the viewpoint window + slidesToScroll: 1, // Dictates the index increment stepping count value advanced on every progression trigger + autoplay: true, // Automates background image cycling processes without demanding user interaction clicks + waitForAnimate: false, // Disables animation queues to allow immediate navigation interactions during active fades + arrows: false, // Suppresses the native left/right side navigation arrow buttons to clean up visual clutter }; return ( + // Mounts the Slider wrapper, spreading configuration rules and infusing standard Infima hero layout classes + {/* --- CAROUSEL SLIDE ITEM 1 --- */}
- + {/* Re-injects the dynamic text overlay structure directly over the active slide index */} Hero slide showing Scouts in action
+ {/* --- CAROUSEL SLIDE ITEM 2 --- */}
Hero slide showing Scouts in action
+ {/* --- CAROUSEL SLIDE ITEM 3 --- */}
Hero slide showing Scouts in action diff --git a/src/components/HeroCarousel/index.module.css b/src/components/HeroCarousel/index.module.css index fe9fb69..acfb880 100644 --- a/src/components/HeroCarousel/index.module.css +++ b/src/components/HeroCarousel/index.module.css @@ -1 +1,18 @@ -@media screen and (max-width:996px){.heroBanner{padding:0}}.buttons{display:flex;align-items:center;justify-content:center} \ No newline at end of file +/* ============================================================================== + CSS Module Stylesheet: Hero Carousel Typography Overlays + Controls layout structures for action elements and responsive mobile boundaries. + ============================================================================== */ + +/* Responsive media query gate handling layout shifts for smaller viewports */ +@media screen and (max-width: 996px) { + .heroBanner { + padding: 0; /* Strips standard Infima hero padding on tablet and mobile screens to expand the slider full-width */ + } +} + +/* Call-to-action button alignment layout panel box container */ +.buttons { + display: flex; /* Invokes Flexbox layout properties to easily align action triggers side-by-side */ + align-items: center; /* Centers button elements perfectly along vertical coordinate balancing nodes */ + justify-content: center; /* Centers button elements perfectly along the horizontal alignment tracking grid */ +} \ No newline at end of file diff --git a/src/components/HomepageFeatures/index.jsx b/src/components/HomepageFeatures/index.jsx index 8ad142d..96d258b 100644 --- a/src/components/HomepageFeatures/index.jsx +++ b/src/components/HomepageFeatures/index.jsx @@ -11,9 +11,9 @@ */ import clsx from "clsx"; -import styles from "./styles.module.css"; -import Link from "@docusaurus/Link"; -import Heading from "@theme/Heading"; +import styles from "./styles.module.css"; // Scoped CSS Modules styling sheet managing custom cards and hover states +import Link from "@docusaurus/Link"; // Docusaurus optimized internal link router to prevent page refreshes +import Heading from "@theme/Heading"; // Structural theme heading component enforcing standard semantic HTML tags /** * Core dataset representing individual scouting unit details. @@ -22,8 +22,9 @@ import Heading from "@theme/Heading"; const FeatureList = [ { title: "Troop 303", + // Webpack resolves this image path from the static directory at compilation build time Jpg: require("@site/static/img/feature-cards/troop303.jpg").default, - UnitSite: "/troop-303", + UnitSite: "/troop-303", // Target destination route for the Boys/Girls Troop page description: ( <> Serving young men ages 11–17 on their journey to Eagle Scout and beyond. @@ -32,8 +33,9 @@ const FeatureList = [ }, { title: "Troop 331", + // Resolves and caches the file asset bundle during the deployment optimization process Jpg: require("@site/static/img/feature-cards/troop331.jpg").default, - UnitSite: "/troop-331", + UnitSite: "/troop-331", // Target destination route for the All-Girl Troop page description: ( <> Providing adventure, leadership, and service opportunities for girls ages 11–17. @@ -42,8 +44,9 @@ const FeatureList = [ }, { title: "Crew 303", + // Resolves the high-adventure co-ed branch image reference location block Jpg: require("@site/static/img/feature-cards/crew303.jpg").default, - UnitSite: "/crew-303", + UnitSite: "/crew-303", // Target destination route for the Venturing Crew page description: ( <> High adventure, leadership, and service opportunities for young men and women ages 14–20. @@ -52,8 +55,9 @@ const FeatureList = [ }, { title: "Pack 303", + // Resolves the entry-level elementary school program image path resource Jpg: require("@site/static/img/feature-cards/pack303.jpg").default, - UnitSite: "/pack-303", + UnitSite: "/pack-303", // Target destination route for the Cub Scout Pack page description: ( <> Starting the journey of Scouting with fun and adventure for boys and girls in grades K–5. @@ -77,12 +81,18 @@ const FeatureList = [ */ function Feature({ Jpg, UnitSite, title, description }) { return ( + // Allocates exactly 3 out of 12 grid spaces per element, making a clean 4-column layout layout grid
+ {/* Link container wrapping the entire card graphic canvas to handle click routing events */} + {/* Background display banner image asset representing the unit */} {title} + {/* Animated visual display box absolute-positioned directly over the image surface */}
+ {/* Semantic h3 header component block displaying the current active unit string */} {title} + {/* Paragraph block summarizing the membership parameters and unit goals */}

{description}

@@ -101,14 +111,19 @@ export default function HomepageFeatures() { return ( <> {" "} + {/* Centered layout row segment initializing section text header indicators */}
Scouting Units
+ {/* Root section context viewport element utilizing module styles layouts */}
+ {/* Fixed horizontal margin spacing box aligning content frames with global layout layouts */}
+ {/* Standard row flex design structure wrapping column grid elements safely */}
+ {/* Loops over the FeatureList matrix records to dynamically inject custom cards onto the page DOM */} {FeatureList.map((props, idx) => ( - + // Spreads data properties object keys and passes unique index numbers for React diff tracking ))}
diff --git a/src/components/HomepageFeatures/styles.module.css b/src/components/HomepageFeatures/styles.module.css index 1bc647c..049eb00 100644 --- a/src/components/HomepageFeatures/styles.module.css +++ b/src/components/HomepageFeatures/styles.module.css @@ -1 +1,108 @@ -.features{padding-bottom:10px}.imageContainer{position:relative;display:inline-block;overflow:hidden;border-radius:8px;background-color:var(--ifm-color-black);height:200px;width:200px}.featureJpg{height:100%;width:100%;object-fit:cover;transition:transform .4s,opacity .4s;display:block}.overlayContent{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:15px;z-index:2}.overlayDescription,.overlayHeading{color:var(--ifm-color-white);text-align:center;z-index:3;margin:0;position:absolute}.overlayHeading{bottom:5px;left:0;right:0;text-shadow:2px 2px 4px rgba(0,0,0,.8);transition:bottom .4s,transform .4s}.overlayDescription{top:50%;left:50%;transform:translate(-50%,-50%);width:90%;opacity:0;transition:opacity .4s;font-size:.85rem;line-height:1.4}.imageContainer:hover .featureJpg{transform:scale(1.1);opacity:.5}.imageContainer:hover .overlayHeading{bottom:75%}.imageContainer:hover .overlayDescription{opacity:1}.imageContainer::after{content:'';position:absolute;inset:0;background-color:rgba(0,0,0,.2);transition:background-color .4s;z-index:1}.imageContainer:hover::after{background-color:rgba(0,0,0,.6)} \ No newline at end of file +/* ============================================================================== + CSS Module Stylesheet: Scouting Unit Grid Feature Cards + Handles responsive spacing, image masking layers, and hover slide-up motions. + ============================================================================== */ + +/* Core workspace block wrapping the entire features collection section layout */ +.features { + padding-bottom: 10px; /* Subtle clearance buffer below the unit section canvas block */ +} + +/* Structural bounding anchor cell wrapping the image graphics and content overlays */ +.imageContainer { + position: relative; /* Establishes absolute positioning boundary context for child content overlays */ + display: inline-block; /* Shrinks bounds tightly around content dimensions preventing width leakage loops */ + overflow: hidden; /* Clips background photos and scaling animations safely within the border radius */ + border-radius: 8px; /* Modern rounded corner radius geometry matching global card layouts */ + background-color: var(--ifm-color-black); /* Enforces dark background underlays to boost image opacity blend styles */ + height: 200px; /* Sets hard locked pixel dimensions creating square card geometry grids */ + width: 200px; +} + +/* Background scouting unit feature presentation card illustration properties */ +.featureJpg { + height: 100%; + width: 100%; + object-fit: cover; /* Scales and center-crops photo frames safely without distorting core aspect ratios */ + transition: transform .4s, opacity .4s; /* Synchronized hardware-accelerated transition curves handling user animations */ + display: block; /* Suppresses inline baseline tracking spaces underneath the image container canvas */ +} + +/* Absolute layout container block overlaying typography directly on top of the image canvas */ +.overlayContent { + position: absolute; /* Unlocks placement tracking values independent of standard document flow schemas */ + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; /* Enforces Flexbox layouts to balance layout layers along centralized paths */ + flex-direction: column; + justify-content: center; /* Groups layout layers along the primary vertical directory stacking axis */ + align-items: center; /* Centers layout layers along horizontal screen space alignment coordinates */ + padding: 15px; /* Internal boundary spacing buffer preventing typography clips on small screens */ + z-index: 2; /* Lifts core layout layer up above background photos and darkening overlay panels */ +} + +/* Shared typography alignment parameters targeting unit headings and descriptions simultaneously */ +.overlayDescription, +.overlayHeading { + color: var(--ifm-color-white); /* Enforces a pure high-contrast text color palette rule across layout spaces */ + text-align: center; /* Centers multi-line text blocks across screen layouts */ + z-index: 3; /* Keeps text properties explicitly stacked over dynamic asset overlay surfaces */ + margin: 0; /* Clears default paragraph block text boundaries to maintain clean calculation nodes */ + position: absolute; /* Allows individual elements to slide independently during runtime interactions */ +} + +/* Unit identity tag heading configuration layout block parameters */ +.overlayHeading { + bottom: 5px; /* Initial resting anchor placement pinned right against the card baseline edge floor */ + left: 0; + right: 0; + text-shadow: 2px 2px 4px rgba(0, 0, 0, .8); /* High-density dark text dropshadow ensuring text visibility on bright image surfaces */ + transition: bottom .4s, transform .4s; /* Fluid transformation timers monitoring upward positioning drift speeds */ +} + +/* Narrative description summary paragraph details typography element block */ +.overlayDescription { + top: 50%; /* Centers block vertical alignments inside the container grid space */ + left: 50%; + transform: translate(-50%, -50%); /* Employs precise offset translations to achieve true pixel alignment balance */ + width: 90%; /* Retains thin margins along left/right coordinate walls to protect layout padding */ + opacity: 0; /* Defaults visibility to zero to keep text hidden on initial load states */ + transition: opacity .4s; /* Fades paragraph lines in smoothly when interactive hover events occur */ + font-size: .85rem; + line-height: 1.4; /* Extends spatial row spacing to optimize small-text readability scores */ +} + +/* --- INTERACTIVE HOVER MOTION TRIGGERS --- */ + +/* Zooms background photos inward and drops opacity values to reveal dark underlays */ +.imageContainer:hover .featureJpg { + transform: scale(1.1); /* Magnifies illustration scaling dimensions outwards by exactly 10% */ + opacity: .5; /* Dims image density values by 50% to optimize dark backdrop contrast levels */ +} + +/* Slides unit title headings cleanly upward from the baseline footer track to make room for descriptions */ +.imageContainer:hover .overlayHeading { + bottom: 75%; /* Pulls heading title string lines up into the top vertical quadrant block */ +} + +/* Transitions summary descriptions from completely hidden fields into fully legible content layouts */ +.imageContainer:hover .overlayDescription { + opacity: 1; /* Sets text opacity values to absolute visibility standard rules */ +} + +/* Dynamic CSS pseudo-element dark glass panel overlay covering background elements */ +.imageContainer::after { + content: ''; /* Injects an empty layout canvas layer block into the document node list */ + position: absolute; + inset: 0; /* Sets full-coverage constraints pinning top, bottom, left, and right coordinates to zero */ + background-color: rgba(0, 0, 0, .2); /* Soft subtle darkening shading layer to gently dim raw illustration file colors */ + transition: background-color .4s; /* Handles smooth color transition pacing during interactions */ + z-index: 1; /* Layered explicitly underneath typography objects to protect visibility tracks */ +} + +/* Deepens dark backdrop glass values when users target the card workspace to enforce absolute text readability */ +.imageContainer:hover::after { + background-color: rgba(0, 0, 0, .6); /* Scales mask opacity up to a deep 60% tint layout overlay rule */ +} diff --git a/src/components/Map/index.jsx b/src/components/Map/index.jsx index 804bad0..aee0a71 100644 --- a/src/components/Map/index.jsx +++ b/src/components/Map/index.jsx @@ -13,13 +13,13 @@ */ import React from "react"; -import BrowserOnly from "@docusaurus/BrowserOnly"; -import troop_icon from "/img/map-marker/troop-marker.png"; -import cub_icon from "/img/map-marker/cub-marker.png"; -import Heading from "@theme/Heading"; +import BrowserOnly from "@docusaurus/BrowserOnly"; // Docusaurus isolation utility that prevents code from executing on Node.js servers +import troop_icon from "/img/map-marker/troop-marker.png"; // Custom map pin illustration graphic path for Boy/Girl Troops +import cub_icon from "/img/map-marker/cub-marker.png"; // Custom map pin illustration graphic path for Cub Scout Packs +import Heading from "@theme/Heading"; // Structural theme heading component enforcing standard semantic HTML tags /** - * Generates the map canvas engine, pins geolocation coordinate items, and configurations + * Generates the map canvas engine, pins geolocation coordinate items, and configures * custom PNG image sizes for mapping markers. Runs explicitly in client context. * * @component @@ -29,51 +29,62 @@ import Heading from "@theme/Heading"; * @returns {React.JSX.Element} Leaflet container node engine matching the assigned dimensions. */ function Map() { + // CRITICAL: Require mapping dependencies inline inside this client-only context. + // Standard top-level ES6 imports would crash Node.js at build time because Leaflet demands a browser 'window' object. const L = require("leaflet"); const { MapContainer, TileLayer, Marker, Popup } = require("react-leaflet"); - require("leaflet/dist/leaflet.css"); + require("leaflet/dist/leaflet.css"); // Imports Leaflet's mandatory core structural CSS layout rules + // Configures sizing matrices for the dynamic Troop map marker vector pin graphics const troopIcon = new L.Icon({ iconUrl: troop_icon, - iconSize: [46, 70], // width, height in pixels - iconAnchor: [23, 70], // point of icon that corresponds to marker's location - popupAnchor: [0, -70], // point from which popup should open relative to iconAnchor + iconSize: [46, 70], // Width and height bounding dimensions of the target icon file in pixels + iconAnchor: [23, 70], // The specific pixel location node [X, Y] aligned directly over the geographic coordinate point + popupAnchor: [0, -70], // Coordinate offset calculation determining where information cards pop open relative to the anchor }); + // Configures sizing matrices for the dynamic Cub Scout map marker vector pin graphics const cubIcon = new L.Icon({ iconUrl: cub_icon, - iconSize: [46, 70], // width, height in pixels - iconAnchor: [23, 70], // point of icon that corresponds to marker's location - popupAnchor: [0, -70], // point from which popup should open relative to iconAnchor + iconSize: [46, 70], + iconAnchor: [23, 70], + popupAnchor: [0, -70], }); return ( + // Base layout node initializing map canvas center view calculations and default resolution tracking zoom multipliers + {/* Fetches and renders OpenStreetMap's geographical background terrain illustration tiles dataset */} - - + + {/* --- MARKER 1: American Legion Post 331 (Troop Meeting Site) --- */} + + {/* Floating display tooltip card opening on user interaction paths */} American Legion Post 331 + {/* Universal high-contrast direction routing query mapping string targeting native tracking apps */} + target="_blank" // Spawns links within completely clean browser subwindows + rel="noopener noreferrer"> // Security parameters masking data leakage transfers across foreign servers Get Directions + + {/* --- MARKER 2: Eagle Elementary School (Pack Meeting Site) --- */} Eagle Elementary School Get Directions @@ -92,7 +103,9 @@ function Map() { */ export default function MapWrapper() { return ( + // Replaces the element during build steps with a static text block until browser engines activate execution pipelines Loading Map...
}> + {/* Lazy-mounts the complete map framework only inside active web browser clients */} {() => } ); diff --git a/src/components/MeetingLocations/index.jsx b/src/components/MeetingLocations/index.jsx index 1535721..2187703 100644 --- a/src/components/MeetingLocations/index.jsx +++ b/src/components/MeetingLocations/index.jsx @@ -11,9 +11,9 @@ */ import React from "react"; -import PackPng from "@site/static/img/logos/pack-icon.png"; -import TroopsPng from "@site/static/img/logos/troop-icon.png"; -import MapWrapper from "@site/src/components/Map"; +import PackPng from "@site/static/img/logos/pack-icon.png"; // Local asset compilation path for the Cub Scout branding icon +import TroopsPng from "@site/static/img/logos/troop-icon.png"; // Local asset compilation path for the Scouts BSA branding icon +import MapWrapper from "@site/src/components/Map"; // Imports the client-side safe isolated interactive map canvas frame /** * Renders an informational split layout section for organization meeting details and maps. @@ -23,31 +23,41 @@ import MapWrapper from "@site/src/components/Map"; */ export default function MeetingLocations() { return ( + // Top-level Flexbox grid dashboard layout workspace bounding container
- {/* Left Side: Clean Visual Information Cards */} + {/* ------------------------------------------------------------------- + LEFT SIDE: Clean Visual Information Cards Panel + ------------------------------------------------------------------- */} + {/* Allocates a flexible base width of 450px; stacks information cards into a tight vertical layout */}
- {/* Card 1: Pack 303 */} + {/* --- CARD 1: Cub Scout Pack 303 Schedule Info Block --- */}
+ {/* Section heading displaying branding colors, using inline flex layout properties to align vector icon graphics */}

Pack Icon Cub Scout Pack 303 (Grades K-5)

+ {/* Weekly calendar schedule entry parameters block */}

When: Tuesdays (School Year) | 6:45 PM – 7:45 PM

+ {/* Geographic baseline physical location description typography parameter lines */}

πŸ“ Eagle Elementary School
555 Sycamore St, Brownsburg, IN 46112

- {/* Card 2: Troops 303 & 331 */} + {/* --- CARD 2: Troops 303 & 331 Schedule Info Block --- */}
+ {/* Section heading displaying Scouts BSA olive branding theme colors across localized layouts */}

Troop Icon Troops 303 (Boys) & 331 (Girls) (Ages 11-17)

+ {/* Year-round calendar schedule entry parameters block */}

When: Every Tuesday (Year-Round) | 6:30 PM – 8:00 PM

+ {/* Geographic baseline physical location description typography parameter lines */}

πŸ“ American Legion Post 331
636 E Main St, Brownsburg, IN 46112 @@ -56,9 +66,12 @@ export default function MeetingLocations() {

- {/* Right Side: Interactive Map */} + {/* ------------------------------------------------------------------- + RIGHT SIDE: Client-Safe Interactive Map Canvas Component + ------------------------------------------------------------------- */} + {/* Allocates a flexible base width of 350px; clips map dimensions inside uniform borders and shadows */}
- + {/* Instantiates the live client-facing geolocation map mapping workspace frame */}
diff --git a/src/components/PhotoAlbumGallery/index.jsx b/src/components/PhotoAlbumGallery/index.jsx index 58262d6..e03948c 100644 --- a/src/components/PhotoAlbumGallery/index.jsx +++ b/src/components/PhotoAlbumGallery/index.jsx @@ -20,6 +20,7 @@ import Counter from "yet-another-react-lightbox/plugins/counter"; import Thumbnails from "yet-another-react-lightbox/plugins/thumbnails"; import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen"; +// Import mandatory structural vendor stylesheets to render album columns and media players accurately import 'react-photo-album/styles.css'; import 'yet-another-react-lightbox/styles.css'; import "yet-another-react-lightbox/plugins/counter.css"; @@ -38,67 +39,78 @@ import "yet-another-react-lightbox/plugins/thumbnails.css"; * */ export default function PhotoAlbumGallery({ context }) { - const [index, setIndex] = useState(-1); - const [photos, setPhotos] = useState([]); + // --- React State Hook Definitions --- + const [index, setIndex] = useState(-1); // Active slider slide tracking pointer (-1 indicates the lightbox is currently closed) + const [photos, setPhotos] = useState([]); // Storage matrix holding array objects of image data (src, width, height, alt) useEffect(() => { + // 1. Map out raw files from the passed Webpack require.context asset bundle map const files = context.keys().map((key) => ({ - src: context(key).default, - alt: key.replace('./', ''), + src: context(key).default, // Extracts compiled production-ready hashed public asset URL strings + alt: key.replace('./', ''), // Normalizes filenames by clearing baseline relative path markers })); + // 2. Wrap each image loader process inside an async Promise block to calculate dimensions safely const loadDimensions = files.map((file) => { return new Promise((resolve) => { - const img = new Image(); - img.src = file.src; + const img = new Image(); // Spawns an unmounted HTMLImageElement memory thread execution context + img.src = file.src; // Initiates background file loading processes over network threads + + // Success execution hook fired immediately after file bytes finish loading on the server img.onload = () => { resolve({ src: file.src, - width: img.naturalWidth || 4, - height: img.naturalHeight || 3, + width: img.naturalWidth || 4, // Extracts pure physical pixel width properties (falls back to a 4:3 default index if zero) + height: img.naturalHeight || 3, // Extracts pure physical pixel height properties alt: file.alt, }); }; + + // Fail-safe tracking exception gate handling missing or broken image assets img.onerror = () => { - resolve({ src: file.src, width: 4, height: 3, alt: file.alt }); + resolve({ src: file.src, width: 4, height: 3, alt: file.alt }); // Resolves default 4:3 boxes to avoid breaking masonry layout algorithms }; }); }); + // 3. Complete all concurrent dimension calculations before updating the state hook matrix data layout Promise.all(loadDimensions).then((resolvedPhotos) => { - setPhotos(resolvedPhotos); + setPhotos(resolvedPhotos); // Overrides local state array data, driving visual layout updates }); - }, [context]); + }, [context]); // Triggers execution pass adjustments if context directory paths change + // --- Conditional UI Render Guard --- if (photos.length === 0) { return

Scanning folder assets...

; } return ( <> + {/* Dynamic layout engine arranging photo blocks flush without uneven rows gap drops */} { - if (containerWidth < 400) return 4; - if (containerWidth < 800) return 5; - return 6; + // Dynamic layout calculation tracking screen grid widths to adjust responsive columns scaling maps + if (containerWidth < 400) return 4; // Tiny screen layouts / mobile panels + if (containerWidth < 800) return 5; // Mid-scale screens / tablets + return 6; // Desktop display interfaces }} - onClick={({ index }) => setIndex(index)} + onClick={({ index }) => setIndex(index)} // Updates the active state index to pop open the corresponding lightbox slide /> + {/* Fullscreen modal media slider component block overlapping global page contexts */} = 0} - index={index} - close={() => setIndex(-1)} - plugins={[Thumbnails, Counter, Fullscreen]} + slides={photos} // Maps target slides asset registry list directly to slide items + open={index >= 0} // Open parameter visibility gate checking if index tracker is awake + index={index} // Set core presentation frame view index focus node + close={() => setIndex(-1)} // Deactivates visibility flags completely on close event execution triggers + plugins={[Thumbnails, Counter, Fullscreen]} // Mounts secondary core navigation plugin utilities modules layers thumbnails={{ - position: "bottom", // Places the carousel row under the main picture - showToggle: false, // Disables and removes the user hide/show button + position: "bottom", // Places the carousel row under the main picture frame container workspace + showToggle: false, // Disables and removes the user hide/show icon button layout elements entirely }} /> ); } - diff --git a/src/components/RecruitmentCards/index.jsx b/src/components/RecruitmentCards/index.jsx index 945b1c0..6491bd8 100644 --- a/src/components/RecruitmentCards/index.jsx +++ b/src/components/RecruitmentCards/index.jsx @@ -68,31 +68,34 @@ const units = [ */ function UnitCard({ title, age, link, bgColor, buttonColor, badgeBg, imgSrc, altText}) { return ( + // Binds inline styling configurations to shape the core layout card footprint layout
+ {/* Top Container grouping branding elements together */}
{altText}

{title}

+ {/* Visual badge element highlighting target age brackets and grade levels */}
+ {/* External link registration anchor button */} { - e.currentTarget.style.transform = 'translateY(-2px)'; - e.currentTarget.style.filter = 'brightness(1.08)'; + e.currentTarget.style.transform = 'translateY(-2px)'; // Gives card button a floating lift look + e.currentTarget.style.filter = 'brightness(1.08)'; // Gently brightens button color highlights }} onMouseOut={(e) => { - e.currentTarget.style.transform = 'translateY(0)'; - e.currentTarget.style.filter = 'brightness(1)'; + e.currentTarget.style.transform = 'translateY(0)'; // Restores default translation points + e.currentTarget.style.filter = 'brightness(1)'; // Re-establishes base canvas brightness limits }} > + {/* Programmatic string parsing splitting text strings to slice unit names dynamically */} Join {title.includes('Pack') ? 'Pack 303' : title.split(' ').slice(1).join(' ')}
@@ -145,44 +151,54 @@ function UnitCard({ title, age, link, bgColor, buttonColor, badgeBg, imgSrc, alt */ export default function SignUpCards({ youtubeId }) { return ( - + // Main centering section component limits max workspace boundary width to a clean 1100px grid path
- {/* 1. Video Player Container */} + + {/* 1. Video Player Framing Canvas Container */}
+ {/* Universal aspect ratio hack ensuring frames scaling calculations preserve an absolute 16:9 box matrix format */}
- {/* 2. Unified Grid Header */} + {/* 2. Unified Text Sub-Header Info Container */}

Ready to Register?

Select your unit below to sign up online today.

- {/* 3. Three-Card Flexbox Grid */} -
- {units.map((unit, idx) => ( - + {/* 3. Three-Card Alignment Layout Grid flex-row workspace element */} +
+ {/* Steps across the units dataset map to automatically build and append cards onto the page DOM */} + {units.map((unit, index) => ( + // Spreads configuration attributes and binds index numbers for React diff engine tracking ))}
-
); -} +} \ No newline at end of file diff --git a/src/components/TroopLeadership/index.jsx b/src/components/TroopLeadership/index.jsx index 2ae3a0f..fa915c7 100644 --- a/src/components/TroopLeadership/index.jsx +++ b/src/components/TroopLeadership/index.jsx @@ -9,7 +9,7 @@ * @requires ./styles.module.css */ import React from 'react'; -import styles from './styles.module.css'; +import styles from './styles.module.css'; // Scoped CSS Modules stylesheet managing grid properties, highlights, and fallback filters /** * Structural definition representing data shapes for individual youth Scouts. @@ -51,14 +51,17 @@ import styles from './styles.module.css'; */ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { let rawNameString = ""; + + // Normalizes dynamic input typing variations down into a standard raw lookup text string if (typeof name === 'string') { rawNameString = name; } else if (typeof name === 'object' && name !== null && typeof name.name === 'string') { rawNameString = name.name; } - // Sanitized fallback to catch blank objects, empty strings, and empty arrays + // Sanitized safety gates to flag missing names, empty listings arrays, or structural placeholder objects const hasNoName = !name || rawNameString.trim() === "( Scout)" || (Array.isArray(name) && name.length === 0); + // Evaluates state check flags to instantly discover if the leadership position is unassigned (Open) const isOpen = hasNoName || rawNameString.toLowerCase() === "open position"; /** @@ -67,16 +70,20 @@ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { * @returns {React.JSX.Element} A contextual metadata layer adjusting typography coloring parameters. */ const renderNameContent = () => { + // Condition 1: Position is vacant -> Renders a clean styled warning notification block if (isOpen) { return
Open Position
; } + // Condition 2: Multi-person position assignment input type -> Maps out an ordered roster list block if (Array.isArray(name)) { return (
{name.map((n, idx) => { + // Unpacks items checks to determine if listing array nodes are objects or raw strings let itemText = typeof n === 'object' ? n.name : n; + // Evaluates youth properties to append rank tags (e.g., "(Eagle Scout)") while avoiding "(Scout Scout)" redundancy let rankText = ''; if (typeof n === 'object' && n.rank) { rankText = n.rank === 'Scout' ? ' (Scout)' : ` (${n.rank} Scout)`; @@ -92,6 +99,7 @@ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { ); } + // Condition 3: Single assignment structure wrapped explicitly within a ScoutObject mapping layout if (typeof name === 'object' && name !== null) { let rankText = ''; if (name.rank) { @@ -100,21 +108,26 @@ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { return
{name.name}{rankText}
; } + // Condition 4: Basic string fallback option for standard adult or singular text profiles return
{name}
; }; return ( + // Dynamic class assignment that shifts layout opacity metrics if isOpen evaluates to a true condition flag
+ {/* Structural profile patch illustration or user placeholder portrait graphic */} {title} + {/* Information text layout compartment box container */}

{title}

- {renderNameContent()} + {renderNameContent()} {/* Dynamically computes name layout text structures via helper loops */}

{description}

+ {/* Conditional rendering block overlaying youth leadership-to-adult mentoring tracking trails */} {mentoredBy && (

🀝 Mentored by: {mentoredBy} @@ -135,6 +148,7 @@ export const RoleCard = ({ title, name, description, img, mentoredBy }) => { * @returns {React.JSX.Element} A layout grid container enforcing flexible spacing guidelines. */ export const Grid = ({ children }) => { + // Injects structural flexbox or responsive CSS grid layout styles across child cell bundles return

{children}
; }; @@ -149,32 +163,45 @@ export const Grid = ({ children }) => { * @returns {React.JSX.Element} A separate display cell visualizing localized patrol assets. */ export const PatrolCard = ({ patrol }) => { + // Guard validation parsing checks scanning patrol leader records to flag empty vacancies const plOpen = !patrol.patrolLeader?.name || patrol.patrolLeader.name.trim() === ""; + + // Computes formatting logic to build the Patrol Leader's youth rank string segment const plRank = patrol.patrolLeader?.rank ? (patrol.patrolLeader.rank === 'Scout' ? ' (Scout)' : ` (${patrol.patrolLeader.rank} Scout)`) : ''; + + // Computes formatting logic to build the Troop Guide / Youth Mentor rank string segment const youthMentorRank = patrol.patrolYouthMentor?.rank ? (patrol.patrolYouthMentor.rank === 'Scout' ? ' (Scout)' : ` (${patrol.patrolYouthMentor.rank} Scout)`) : ''; return (
+ {/* Patrol patch asset graphic (e.g., Fox, Eagle, or Wolf icon patch) */} {`${patrol.patrolName} + {/* Inner profile text tracking compartment container wrapper */}

{patrol.patrolName} Patrol

+ + {/* Patrol Leader Display Field: Dynamic text switch highlighting open vacancies in red */}

Patrol Leader:{' '} {plOpen ? "Open Position" : `${patrol.patrolLeader.name}${plRank}`}

+ + {/* Youth Mentor Display Field: Lists Assigned Troop Guide or Senior Youth Advisor */}

Youth Mentor: {patrol.patrolYouthMentor?.name || "None"}{youthMentorRank}

+ + {/* Adult Mentor Display Field: Lists Assigned Assistant Scoutmaster or Patrol Counselor */}

Adult Mentor: {patrol.patrolAdultMentor?.name || "None"}

diff --git a/src/components/TroopLeadership/styles.module.css b/src/components/TroopLeadership/styles.module.css index 1b8915f..7ecf789 100644 --- a/src/components/TroopLeadership/styles.module.css +++ b/src/components/TroopLeadership/styles.module.css @@ -1,87 +1,104 @@ +/* ============================================================================== + CSS Module Stylesheet: Leadership & Patrol Roster Cards + Configures a fluid, responsive auto-fitting grid system using custom brand + theming color tokens for text headers, profile images, and vacant roles. + ============================================================================== */ + /* Card Container Grid Layout */ .grid { display: grid; + /* Automatically generates columns that wrap fluidly down to 320px minimum widths, stretching to fill available viewport rows */ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 20px; + gap: 20px; /* Consistent channel margins dividing neighboring cell blocks */ margin-top: 16px; margin-bottom: 32px; } -/* Base Role Card Layout */ +/* Base Role Card Layout Panel */ .roleCard { - background-color: var(--scouting-america-light-tan); - border: 1px solid var(--scouting-america-dark-tan); - border-radius: 12px; - padding: 20px; - box-shadow: var(--global-box-shadow); - display: flex; - gap: 16px; - align-items: flex-start; - transition: transform 0.2s ease, box-shadow 0.2s ease; + background-color: var(--scouting-america-light-tan); /* Soft thematic sand background tone */ + border: 1px solid var(--scouting-america-dark-tan); /* Thin framing outline boundary stroke */ + border-radius: 12px; /* Modern curved panel corner profile geometry */ + padding: 20px; /* Uniform layout cushions separating contents from card frame walls */ + box-shadow: var(--global-box-shadow); /* Injects smooth site-wide elevation depth casting drop-shadow values */ + display: flex; /* Establishes dynamic Flexbox rows to position profile pictures beside text logs */ + gap: 16px; /* Air gap margin spacing separating the image from the text box block */ + align-items: flex-start; /* Vertical alignment lock keeping child elements flush with the card roof line */ + transition: transform 0.2s ease, box-shadow 0.2s ease; /* Fluid hardware-accelerated tracking timings for layout shifts */ } /* Open Position State Modifier */ .roleCardOpen { - background-color: var(--scouting-america-tan); - border: 1px dashed var(--scouting-america-dark-tan); + background-color: var(--scouting-america-tan); /* Shifts backing to a deeper neutral shade to emphasize vacancy */ + border: 1px dashed var(--scouting-america-dark-tan); /* Transitions solid line outlines to an explicit dashed pattern style */ } /* User & Profile Image Styles */ .cardImg { - width: 64px; + width: 64px; /* Strict square aspect bounding box framework dimensions */ height: 64px; - border-radius: 50%; - object-fit: cover; + border-radius: 50%; /* Transforms source square graphics into clean circular profile portrait views */ + object-fit: cover; /* Centers and masks raw files safely to protect human body aspect ratios */ background-color: var(--scouting-america-light-tan); - border: 2px solid var(--scouting-america-dark-tan); - flex-shrink: 0; + border: 2px solid var(--scouting-america-dark-tan); /* Strong framing border stroke encapsulating the portrait circle */ + flex-shrink: 0; /* Layout lock: prevents horizontal text layout pressures from squishing the portrait */ } +/* Open Position Profile Picture Modifier */ .cardImgOpen { - border: 2px dashed var(--scouting-america-dark-tan); + border: 2px dashed var(--scouting-america-dark-tan); /* Convers user placeholder boundaries to dashed paths matching the open state */ } /* Text & Typography Layout Elements */ .cardContent { - flex-grow: 1; + flex-grow: 1; /* Directs the text box compartment to swell outward to absorb all remaining row width spaces */ } +/* Position title heading string configurations */ .titleText { - margin: 0 0 4px 0; + margin: 0 0 4px 0; /* Clears vertical layout padding fields to keep line heights compact */ color: var(--scouting-america-dark-gray); font-size: 1.15rem; - font-weight: 700; - line-height: 1.3; + font-weight: 700; /* Heavy title typography formatting parameter */ + line-height: 1.3; /* Closes leading gaps preventing long headers from drifting across text blocks */ } +/* Roster entity name text layout block line elements */ .nameContainer { font-size: 0.95rem; font-weight: 600; - color: var(--scouting-america-blue); + color: var(--scouting-america-blue); /* Deep high-contrast theme blue color tag indicating an active profile name assignment */ margin: 0 0 2px 0; } +/* Open Position Name Text Modifier */ .nameContainerOpen { - color: var(--scouting-america-red); + color: var(--scouting-america-red); /* Shifts typography colors to a prominent attention-grabbing warning red token */ } +/* Role abstract text description paragraph block layout */ .descText { font-size: 0.875rem; color: var(--scouting-america-dark-gray); - margin: 0 0 12px 0; - line-height: 1.4; + margin: 0 0 12px 0; /* Bottom cushion isolating text summaries away from mentorship metadata tags */ + line-height: 1.4; /* Optimizes sentence line heights to enhance overall paragraph readability values */ } +/* Footnote segment outlining mentor relationships */ .mentorSection { font-size: 0.8rem; color: var(--scouting-america-dark-gray); margin: 0; - border-top: 1px dashed var(--scouting-america-gray); - padding-top: 8px; - font-style: italic; + border-top: 1px dashed var(--scouting-america-gray); /* Fine divider separating main summary paragraphs from mentor rows */ + padding-top: 8px; /* Spacing buffer pushing text below the dashed line axis */ + font-style: italic; /* Slanted editorial character typography configuration */ } -/* Specific Patrol Card Structural Variant */ +/* ============================================================================== + Specific Patrol Card Structural Variant + ============================================================================== */ + +/* Patrol panel card component container block layout */ .patrolCard { border: 1px solid var(--scouting-america-dark-tan); border-radius: 12px; @@ -93,39 +110,45 @@ align-items: flex-start; } +/* Small patch emblem circular element frame formatting */ .patrolBadge { width: 64px; height: 64px; border-radius: 50%; - object-fit: cover; + object-fit: cover; /* Scale locks the emblem patch graphics without clipping critical shape edges */ background-color: var(--scouting-america-light-tan); border: 1px solid var(--scouting-america-dark-tan); - padding: 1px; + padding: 1px; /* Thin inner breathing track between outline borders and patch contents */ flex-shrink: 0; } +/* Section title layout for specific sub-unit patrol groups */ .patrolHeader { margin: 0 0 12px 0; color: var(--scouting-america-dark-gray); - border-bottom: 1px dashed var(--scouting-america-gray); + border-bottom: 1px dashed var(--scouting-america-gray); /* Floor baseline separator strip splitting headers from crew names */ padding-bottom: 4px; } +/* Unified baseline properties handling metadata line outputs inside the card */ .patrolMetaText { - margin: 0 0 6px 0; + margin: 0 0 6px 0; /* Generates short uniform steps to segment name variables lines cleanly */ font-size: 0.9rem; } +/* Terminal meta row entry parameter adjustments to drop trailing space boundaries */ .patrolMetaTextLast { margin: 0; font-size: 0.9rem; } +/* Target highlight override alerting users to vacant patrol leadership gaps */ .openHighlight { - color: var(--scouting-america-red); - font-weight: bold; + color: var(--scouting-america-red); /* Assigns red font color indicators to vacant labels */ + font-weight: bold; /* Enhances character stroke visibility weights */ } +/* Default styling assignment for standard assigned roster entities strings */ .filledText { - color: var(--scouting-america-dark-gray); + color: var(--scouting-america-dark-gray); /* Blends active records to match standard body text outputs smoothly */ } diff --git a/src/components/UpcomingEvents/index.jsx b/src/components/UpcomingEvents/index.jsx index fc4585e..7421d7d 100644 --- a/src/components/UpcomingEvents/index.jsx +++ b/src/components/UpcomingEvents/index.jsx @@ -9,7 +9,7 @@ * @requires @theme/Heading */ -import Heading from "@theme/Heading"; +import Heading from "@theme/Heading"; // Structural theme heading component enforcing standard semantic HTML tags /** * Renders an iframe-based aggregated Google Calendar matching layout container constraints. @@ -20,17 +20,45 @@ import Heading from "@theme/Heading"; */ export default function UpcomingEvents({}) { return ( + // Outer responsive grid framework container centering the layout section block horizontally
+ {/* Centered primary header title using the swappable Infima alignment utility class */} Upcoming Events + + {/* + Embedded Google Calendar Component + Aggregates multiple underlying .ics data feeds into a unified display window. + + URL Parameter Breakdown: + - height=600 / height="600": Establishes locked viewport height allocations in pixels + - wkst=1: Mandates that the calendar grid weeks begin explicitly on Monday + - ctz=America%2FIndiana%2FIndianapolis: Locks timezone coordinates to Eastern Time (US) + - showPrint=0 / showTitle=0 / showTz=0: Cleans UI clutter by disabling print, titles, and timezone text + + Feed Sources (`src` hashes): + - NW1...7sta: Troop 303 Schedule + - en.usa#holiday: Standard United States Public Holidays + - d4v...8rkp: Troop 331 Schedule + - p3u...g97a: Cub Scout Pack 303 Schedule + - rgd...3rea: Venturing Crew 303 Schedule + + Brand Hex Color Palette Maps (`color` codes): + - %23006b3f: Scouts BSA Olive Green (#006b3f) + - %23d6cebd: Scouting America Tan (#d6cebd) + - %23003f87: Cub Scouts Blue (#003f87) + - %23ce1126: Holiday / Alert Red (#ce1126) + - %23fcd116: Cub Scouts Gold (#fcd116) + */}
); } + diff --git a/src/css/custom.css b/src/css/custom.css index 9533aca..6252c12 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -318,4 +318,4 @@ a:hover i.fab { .pagination-nav { display: none !important; -} +} \ No newline at end of file diff --git a/src/pages/crew-303/index.mdx b/src/pages/crew-303/index.mdx index d440595..7b2e0a8 100644 --- a/src/pages/crew-303/index.mdx +++ b/src/pages/crew-303/index.mdx @@ -1,5 +1,5 @@ --- -description: The internet home of Crew 303 +description: "Ready for high adventure? Crew 303 is a co-ed outdoor club for older youth (Ages 14-20) in Brownsburg, IN focusing on advanced camping and peer independence." hide_table_of_contents: true --- diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 9fb1335..8e30401 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -14,12 +14,12 @@ * @requires @site/src/components/UpcomingEvents */ -import Layout from "@theme/Layout"; -import HomepageFeatures from "@site/src/components/HomepageFeatures"; -import UpcomingEvents from "@site/src/components/UpcomingEvents"; -import HeroCarousel from "@site/src/components/HeroCarousel"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import HomepageBlogCards from "@site/src/components/BlogCard"; +import Layout from "@theme/Layout"; // Imports the global Docusaurus scaffolding layout (injects top navigation bars, mobile menus, and site footers) +import HomepageFeatures from "@site/src/components/HomepageFeatures"; // Layout component grid highlighting the active units (Troop 303, Pack 303, etc.) +import UpcomingEvents from "@site/src/components/UpcomingEvents"; // Calendar aggregator component mapping out global group schedules +import HeroCarousel from "@site/src/components/HeroCarousel"; // Autoplay picture slideshow component anchoring home recruitment copy +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // System configuration context hook fetching variables from docusaurus.config.js +import HomepageBlogCards from "@site/src/components/BlogCard"; // Grid section displaying the most recent adventure posts on the site /** * Renders the master homepage structure wrapped within the global Docusaurus layout framework. @@ -29,18 +29,29 @@ import HomepageBlogCards from "@site/src/components/BlogCard"; * @returns {React.JSX.Element} The fully assembled homepage template. */ export default function Home() { + // Pulls system metadata configurations directly out of your repository's central workspace environment config layer const { siteConfig } = useDocusaurusContext(); return ( + // Wraps everything inside the global frame layout, providing clean search engine metadata mapping arguments + {/* Dynamic top carousel block containing background imagery and main onboarding buttons */} + + {/* Semantic main HTML content workspace block containing standard page subdivisions */}
+ {/* Row block section mapping out grid profiles for each charter scouting unit */} + + {/* Row block section displaying a grid of recent blog entries compiled on the server */} + + {/* Row block section mounting the responsive multi-unit shared schedule overview frame */}
); } + diff --git a/src/pages/pack-303/index.mdx b/src/pages/pack-303/index.mdx index f55fd02..e434776 100644 --- a/src/pages/pack-303/index.mdx +++ b/src/pages/pack-303/index.mdx @@ -1,5 +1,5 @@ --- -description: The internet home of Pack 303 +description: "Fun, family-centered camping and life skills for elementary youth (Grades K-5) in Brownsburg, IN. Build a lifetime foundation of leadership with Pack 303!" hide_table_of_contents: true --- diff --git a/src/pages/troop-303/index.mdx b/src/pages/troop-303/index.mdx index 65553b7..3b2678e 100644 --- a/src/pages/troop-303/index.mdx +++ b/src/pages/troop-303/index.mdx @@ -1,5 +1,5 @@ --- -description: The internet home of The Legendary Troop 303 +description: "Join the legendary Troop 303 in Brownsburg, IN! We build leadership and character through high-adventure outdoor camping for young men ages 11-17." hide_table_of_contents: true --- diff --git a/src/pages/troop-331/index.mdx b/src/pages/troop-331/index.mdx index 5f01566..0922dcf 100644 --- a/src/pages/troop-331/index.mdx +++ b/src/pages/troop-331/index.mdx @@ -1,5 +1,5 @@ --- -description: The internet home of Troop 331 +description: "Empowering tomorrow's female leaders. Troop 331 in Brownsburg, IN offers a youth-led outdoor adventure program helping young women achieve the Eagle Scout rank." hide_table_of_contents: true --- diff --git a/src/theme/Footer/index.jsx b/src/theme/Footer/index.jsx index 33f8d32..cda4d73 100644 --- a/src/theme/Footer/index.jsx +++ b/src/theme/Footer/index.jsx @@ -12,6 +12,21 @@ import React from "react"; import Footer from "@theme-original/Footer"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +/** +/** + * @file index.js + * @description A custom wrapper that extends the default Docusaurus footer layout via swizzling. + * Appends a secondary custom copyright split-section utilizing global site configuration fields. + * + * @module FooterWrapper + * @requires React + * @requires @theme-original/Footer + * @requires @docusaurus/useDocusaurusContext + */ +import React from "react"; +import Footer from "@theme-original/Footer"; // Imports the unswizzled core Docusaurus original theme footer blueprint +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // React context hook to fetch metadata arrays out of docusaurus.config.js + /** * Renders the original system footer layout integrated with customized organization compliance details. * @@ -20,17 +35,28 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; * @returns {React.JSX.Element} The original footer element coupled with custom bottom layout panels. */ export default function FooterWrapper(props) { + // Pulls system config context variables directly out of your repository's central runtime layer const { siteConfig } = useDocusaurusContext(); + + // Destructures the custom string fields variables defined inside your docusaurus.config.js object mapping array const { copyright1, copyright2 } = siteConfig.customFields; return ( <> + {/* Renders the un-swizzled standard Docusaurus original theme links matrix, passing all default props downstream safely */}
+ + {/* Custom visual dividing block panel layout separating standard links from extended footer content cells */}
+ + {/* + Custom Split-Section compliance block panel layout. + Outputs additional required organizational metadata text lines right at the baseline floor of the site canvas. + */}
- {copyright1} -
- {copyright2} + {copyright1} {/* Displays custom line 1 text configurations (e.g., Charter organization notices) */} +
{/* Native structural line break pushing downstream properties to a clean row */} + {copyright2} {/* Displays custom line 2 text configurations (e.g., Unified privacy policy or unit compliance headers) */}
); diff --git a/src/theme/Logo/index.jsx b/src/theme/Logo/index.jsx index 4314730..e56d37d 100644 --- a/src/theme/Logo/index.jsx +++ b/src/theme/Logo/index.jsx @@ -14,11 +14,11 @@ */ import React from "react"; -import Link from "@docusaurus/Link"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import { useThemeConfig } from "@docusaurus/theme-common"; -import ThemedImage from "@theme/ThemedImage"; +import Link from "@docusaurus/Link"; // Docusaurus optimized internal link router to prevent page refreshes +import useBaseUrl from "@docusaurus/useBaseUrl"; // Appends the site's configured baseUrl configuration prefix to static paths +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // System configuration context hook fetching variables from docusaurus.config.js +import { useThemeConfig } from "@docusaurus/theme-common"; // Extracts current navigation and theme settings from global client contexts +import ThemedImage from "@theme/ThemedImage"; // Core Docusaurus component capable of hot-swapping images between light and dark mode UI toggles /** * Processes light/dark source values and structures the underlying theme element image canvas. @@ -33,10 +33,13 @@ import ThemedImage from "@theme/ThemedImage"; * @returns {React.JSX.Element} A configured brand image layout node. */ function LogoThemedImage({ logo, alt, imageClassName }) { + // Appends baseUrl routing paths to light and dark source strings dynamically const sources = { light: useBaseUrl(logo.src), - dark: useBaseUrl(logo.srcDark || logo.src), + dark: useBaseUrl(logo.srcDark || logo.src), // Falls back to the light mode image source layout map if dark asset is omitted }; + + // Assembles the core image markup canvas with size dimensions and inline style properties const themedImage = ( ); - // Is this extra div really necessary? - // introduced in https://github.com/facebook/docusaurus/pull/5666 + + // Historical Legacy Inheritance Note: This explicit div block wrapper was introduced + // in Docusaurus PR #5666 to isolate navbar logo boundaries following Infima framework updates. return imageClassName ? ( -
{themedImage}
+
{themedImage}
// Wraps the theme image inside a layout div cell block if custom styles match ) : ( - themedImage + themedImage // Returns the bare image object node if no specific layout rules exist ); } @@ -67,26 +71,37 @@ function LogoThemedImage({ logo, alt, imageClassName }) { * @returns {React.JSX.Element} A link block surrounding the brand theme elements. */ export default function Logo(props) { + // Pulls site meta properties variables out of global tracking layers const { siteConfig: { title }, } = useDocusaurusContext(); + + // Extracts navbar-specific property blocks out of active configuration files (docusaurus.config.js) const { navbar: { title: navbarTitle, logo }, } = useThemeConfig(); + + // Descriptors mapping custom element naming styles from rest arguments properties arrays const { imageClassName, titleClassName, ...propsRest } = props; + + // Calculates base landing page home routes fallback locations links const logoLink = useBaseUrl(logo?.href || "/"); - // If visible title is shown, fallback alt text should be - // an empty string to mark the logo as decorative. + + // Accessibility Engine Logic Check: If a text name is already visible in the navigation header bar, + // setting fallbackAlt to an empty string hides the image asset from screen readers to prevent duplicate text notifications. const fallbackAlt = navbarTitle ? "" : title; - // Use logo alt text if provided (including empty string), - // and provide a sensible fallback otherwise. + + // Prefers user-defined custom override alt tags over calculated automated screen reading templates const alt = logo?.alt ?? fallbackAlt; + return ( + // Wraps brand identity icons inside an optimized single-page routing anchor container link + {/* Checks if a valid logo object tree config map parameter exists before mounting components */} {logo && ( System architecture and formatting rules for LLMs contributing to this +> repository. + +## Project Profile + +- **Stack:** Docusaurus v3+, React, MDX, JavaScript/TypeScript, Python. +- **Audience:** Scouting America members, parents, and leaders. +- **Design Core:** Custom theme using Scouting America branding + (Khaki/Blue/Red). +- **Dark Mode:** Explicitly disabled. Code must only support light mode. +- **Source Code:** Hosted at `https://github.com/scouting331/scoutSite`. +- **Hosting & CI/CD:** Built automatically by a worker on commit and deployed to + `https://brownsburgscouts.org`. +- **Maintenance Philosophy:** Built with extensive automations and thorough + commenting so young youth Scouts with limited coding abilities can easily + maintain the site, while remaining structured cleanly for a seamless future + handoff to experienced adult coders. + +## Organization Profile & Unit Context + +- **Sponsor:** Chartered by American Legion Post 331 in Brownsburg, IN. +- **Core Mission:** Developing tomorrow's leaders through character building, + citizenship training, and personal fitness. +- **Pack 303:** Co-ed Cub Scout Pack for elementary youth (Grades K-5). Focuses + on family camping, foundational leadership skills, and advancement. Meets + weekly during the school year at Eagle Elementary in Brownsburg, IN. +- **Troop 331:** Scouts BSA Girls Troop (Ages 11-17). Focuses on youth-led + leadership and life preparation to build tomorrow's female leaders. The + ultimate achievement for these Scouts is the rank of Eagle. +- **Troop 303:** Scouts BSA Boys Troop (Ages 11-17), known as "The Legendary + Troop 303". Focuses on outdoor adventure and intensive leadership development + to prepare young men for future success. The ultimate achievement for these + Scouts is the rank of Eagle Scout. +- **Crew 303:** Co-ed Venturing Crew for older youth (Ages 14-20). Focuses on + high adventure, high-intensity camping, and advanced youth independence, + continuing the mission of peer leadership. + +## Codebase Map & Router Configurations + +### Content Plugins (Multi-Instance Docs) + +This site uses multiple instances of the Docusaurus docs plugin. + +- `/docs/`: Main instance. Core Scouting unit documents and shared files. Uses + `sidebarsDocs.js`. +- `/cookbook/`: Custom instance. Camping recipes ecosystem. Uses + `sidebarCookbook.js`. + +### Blogs & Components + +- `/blog/`: Unit activity updates. Cross-referenced via `/blog/authors.yml` and + `/blog/tags.yml`. +- `/src/pages/`: Main entry point and unit-specific landing pages. +- `/src/components/[ComponentName]/index.jsx`: Modular UI components. Optional + localized `styles.module.css` inside the same folder. +- `/static/`: Shared media assets (images, PDFs). Reference using absolute paths + (e.g., `/img/logo.png`). +- `/src/css/custom.css`: Target file for global font and CSS variable overrides. + +## Strict Code Conventions + +### Frontmatter Definitions + +AI must generate frontmatter matching these strict schemas: + +#### Docs & Cookbook (`/docs/`, `/cookbook/`) + +```yaml +title: "Page Title" +description: "SEO description under 160 chars" +``` + +#### Blog Posts (`/blog/`) + +```yaml +title: "Post Title" +date: YYYY-MM-DD +authors: [author_key_from_authors_yml] +tags: [unit_tag_from_tags_yml] +``` + +_Allowed tags in `tags.yml`:_ `troop-303`, `troop-331`, `pack-303`, `crew-303`. + +#### MDX Pages (`/src/pages/`) + +```yaml +description: "SEO description" +hide_table_of_contents: true +``` + +### Component & Markup Rules + +- **Formatting:** All narrative content must use `.mdx` extensions. Custom + components must use `.jsx`. +- **Built-ins:** Always default to native ``, ``, and + `` components. +- **Custom HTML:** Never write raw inline HTML in markdown. Wrap HTML into a + reusable component in `/src/components/`. +- **Modification:** Never modify `node_modules`. Use `npm run swizzle` for + layout overrides. + +## CLI Core Commands + +- **Local Dev:** `npm run start` + +## AI Commenting & Documentation Standards + +All generated code (JavaScript, JSX, Python, Bash) must be heavily commented and +highly descriptive. The codebase serves as a learning tool for young Scouts; +assume the reader has zero prior programming experience. + +### Mandatory Structure for Code Files + +#### 1. JSX / React Components (`.jsx`) + +- **Module Level:** A detailed block comment at the top explaining the + component's purpose, its visual location on the site, and any props it + accepts. +- **Inline Comments:** Explicitly explain _why_ a hook is used (`useState`, + `useEffect`) and break down any logical conditions or array mappings. + +```jsx +/** + * AnnouncementsWidget Component + * + * Purpose: Displays a list of recent unit updates on the landing page. + * Audience: Pack 303 / Troop 303 parents looking for schedules. + * Props: + * - limit (Number): Max number of posts to display. + */ +import React from "react"; + +export default function AnnouncementsWidget({ limit = 3 }) { + // Step 1: Set up a state variable to hold the blog posts we fetch + const [posts, setPosts] = React.useState([]); + + return ( +
+ {/* Loop through each post item and convert it into a visual card */} + {posts.slice(0, limit).map((post) => ( +
+

{post.title}

+
+ ))} +
+ ); +} +``` + +#### 2. Python Scripts (`.py`) + +- **Module Level:** Google-style module docstring outlining the script's global + role in the site's automations. +- **Function Level:** Clear docstrings defining `Args` and `Returns` explicitly. +- **Inline Comments:** Step-by-step logic tracking, avoiding implicit code + shortcuts. + +```python +"""Calendar Sync Automation. + +This script pulls events from the American Legion Post 331 Google Calendar +and formats them into Markdown files for the Docusaurus site schedule. +""" + +def parse_calendar_event(event_data): + """Extracts date, time, and unit details from a raw calendar object. + + Args: + event_data (dict): The raw JSON object returned by the Calendar API. + + Returns: + dict: A cleaned dictionary containing 'title', 'date', and 'target_unit'. + """ + # Initialize an empty dictionary to safely structure our clean data + cleaned_event = {} + + # Extract the summary line (e.g., "Pack 303 Blue & Gold Banquet") + cleaned_event['title'] = event_data.get('summary', 'Untitled Event') + + return cleaned_event +``` + +### Strict LLM Rules for Code Generation + +1. **No "Self-Explanatory" Code:** Never skip comments under the assumption that + the code is clean enough to read without them. +2. **Explain the "Why", Not Just the "What":** Instead of writing + `# sets x to 5`, write + `# Set the fallback limit to 5 so the page doesn't break if the API fails`. +3. **Use Simple Analogies:** When explaining complex coding patterns (like git + filters or regex parsing), include a one-sentence conceptual analogy in the + comment block. + +## Scout & Leader Local Execution Guide + +This section ensures that any Scout or leader can safely run our background +automation scripts (like the Facebook router or calendar syncs) on their +personal computer without breaking the main website. + +### 1. Prerequisites (One-Time Setup) + +Before running any Python scripts, the computer needs Python installed and a +copy of the website code. + +1. **Open the Terminal / Command Prompt:** + - **Windows:** Press the `Windows Key`, type `cmd`, and press Enter. + - **Mac:** Press `Cmd + Space`, type `Terminal`, and press Enter. +2. **Check if Python is installed:** Type `python --version` and press Enter. If + it displays a version number (like `Python 3.11.x`), it is ready. +3. **Navigate to the Project Folder:** Use the change directory (`cd`) command + to move into the downloaded repository folder: + + ```bash + cd path/to/scoutSite + ``` + +### 2. Testing Scripts Safely on Your Computer + +To test scripts locally without affecting the live website or sending accidental +posts to the public Facebook pages, always follow these rules: + +#### Step A: Create a Safe Virtual Environment + +A virtual environment keeps our project's tools separate from the rest of the +computer so nothing gets mixed up. + +```bash +# 1. Create the environment folder (we name it 'scout-env') +python -m venv scout-env + +# 2. Turn it on (Activate it) +# On Windows: +scout-env\Scripts\activate +# On Mac / Linux: +source scout-env/bin/activate +``` + +_Visual cue: Your terminal line will now show `(scout-env)` at the very +beginning._ + +#### Step B: Simulate GitHub Environment Variables + +Our automation scripts look for special environment variables that GitHub +normally provides automatically. When testing on a personal computer, you must +simulate them manually in your terminal before running the script: + +```bash +# Create a dummy mock file for the script to write its outputs to +touch mock_output.txt + +# Tell the computer where that mock file lives +# On Windows (Command Prompt): +set GITHUB_OUTPUT=mock_output.txt +# On Mac / Linux: +export GITHUB_OUTPUT=mock_output.txt +``` + +#### Step C: Run the Script + +Now you can safely trigger the script to see how it behaves: + +```bash +python .github/scripts/fb_router.py +``` + +### 3. Common Error Troubleshooting + +If a Scout runs into an issue, match the terminal error message to the solutions +below: + +#### ❌ Error: `"python" is not recognized as an internal or external command` + +- **What it means:** The computer doesn't know where Python is installed. +- **The Fix:** Download and run the official installer from `python.org`. On + Windows, **make sure to check the box that says "Add python.exe to PATH"** at + the very bottom of the installer window before clicking install. + +#### ❌ Error: `KeyError: 'GITHUB_OUTPUT'` + +- **What it means:** The script tried to save its results to GitHub, but it + couldn't find the target output file pathway. +- **The Fix:** You forgot to run the `set GITHUB_OUTPUT` or + `export GITHUB_OUTPUT` command from Step B. Run that command and try again. + +#### ❌ Error: `Error reading git diff.` / `fatal: not a git repository` + +- **What it means:** The script is trying to look at your recent file changes + using Git history, but you either aren't inside the project folder, or Git + isn't tracking it. +- **The Fix:** Make sure you ran `cd scoutSite` to get into the folder. Type + `git status` to verify that your history tracks properly. + +## Scout & Leader Frontend Local Execution Guide + +This section helps Scouts and leaders launch a local preview of the website on +their computer using Node.js and Node Package Manager (npm). This allows them to +see their markdown edits and component changes live before submitting them to +GitHub. + +### 1. Prerequisites (One-Time Setup) + +To build the user interface, the computer needs Node.js installed to process our +React and Docusaurus files. + +1. **Install Node.js:** Download and run the **LTS (Long Term Support)** + installer from `nodejs.org`. +2. **Verify Installation:** Open your Terminal or Command Prompt and run: + + ```bash + node --version + npm --version + ``` + + _If both commands return numbers (e.g., `v20.x.x` and `10.x.x`), you are + ready to go._ + +3. **Download Project Tools:** Navigate into your `scoutSite` folder and + download the exact construction packages required for our theme: + + ```bash + cd path/to/scoutSite + npm install + ``` + +### 2. Launching the Live Preview Website + +Once your folder setup is complete, you can start the local development server +to test your changes. + +```bash +npm run start +``` + +- **What happens next:** The terminal will compile the pages, turn on a tiny + background server, and automatically open a web browser tab pointing to + `http://localhost:3000`. +- **Live Editing:** Leave this terminal window open! Every time you modify and + save a blog post (`.md`), documentation file, or React component (`.jsx`), the + website in your browser will automatically refresh to show your new changes + within two seconds. +- **How to turn it off:** Go back to your terminal window and press `Ctrl + C` + on your keyboard, then type `Y` if prompted, to turn off the local preview. + +### 3. Common Frontend Error Troubleshooting + +If a Scout hits a wall while trying to run the frontend, look for these common +error signatures: + +#### ❌ Error: `'npm' is not recognized as an internal or external command` + +- **What it means:** The computer doesn't know where Node.js or npm is located. +- **The Fix:** Close your terminal window entirely, reopen it, and try again. If + it still fails, reinstall Node.js from the official site and ensure you don't + uncheck any path settings during installation. + +#### ❌ Error: `Error: Cannot find module '...'` or `sh: docusaurus: command not found` + +- **What it means:** The project folder is missing its core files because the + initial dependency download was skipped or interrupted. +- **The Fix:** Run `npm install` inside the project folder. This creates a fresh + `node_modules/` folder containing all the building blocks Docusaurus needs. + +#### ❌ Error: `Port 3000 is already in use` + +- **What it means:** Another local server is already running on your computer, + or an old Docusaurus session didn't close properly. +- **The Fix:** The terminal will usually ask: _"Would you like to run the server + on another port instead? (Y/n)"_. Simply type `Y` and hit Enter. Docusaurus + will open your site at `http://localhost:3001` instead. + +#### ❌ Error: `Docusaurus found broken links!` + +- **What it means:** A Scout added a link to a page, image, or document that + doesn't actually exist in the repository or has a typo in its path. +- **The Fix:** Read the lines directly below the error message in the terminal. + Docusaurus will tell you exactly which markdown file has the broken link and + what typo it is looking for. Correct the path inside that file and save it. + +## Scout & Leader Git & GitHub Contribution Guide + +This guide ensures that any Scout or volunteer leader can safely pull down code, +make changes, and submit their work for review without accidentally breaking the +live website. + +### 1. The 6-Step Scout Contribution Recipe + +Always use a separate "branch" (a sandbox copy) for your work. Never edit the +`main` branch directly. + +#### Step 1: Get the Latest Code + +Before starting any new work, make sure your computer has the absolute newest +copy of the website from the internet: + +```bash +git checkout main +git pull origin main +``` + +#### Step 2: Create Your Sandbox (Branch) + +Create a new branch named after the task you are doing (use dashes instead of +spaces, and keep it lowercase): + +```bash +# Syntax: git checkout -b scout-name-description +git checkout -b john-d-add-pack303-blog +``` + +#### Step 3: Do Your Work and Test It + +Modify your markdown files, add your photos, or edit your code. Use our +execution guides to test your changes locally: + +- Run `npm run start` to make sure your pages look correct and have no broken + links. +- Run your Python scripts locally to verify your automations. + +#### Step 4: Save Your Progress (Commit) + +Tell Git to take a snapshot of the files you changed. Write a clear, simple +message explaining what you did: + +```bash +# 1. Stage all your changed files to be saved +git add . + +# 2. Save the snapshot with a meaningful description +git commit -m "Add blog post for Pack 303 Blue and Gold Banquet" +``` + +#### Step 5: Upload to GitHub + +Send your branch from your personal computer up to our online repository on +GitHub: + +```bash +git push origin john-d-add-pack303-blog +``` + +#### Step 6: Create a Pull Request (PR) + +1. Go to `https://github.com`. +2. You will see a yellow banner at the top that says **"Compare & pull + request"**. Click it. +3. Write a short note describing your changes so an adult leader or experienced + Scout can review it and merge it into the live site! + +--- + +### 2. Common Git Troubleshooting + +#### ❌ Error: `fatal: Grandma's-Macbook is not a git repository` + +- **What it means:** Your terminal is currently pointing to a generic folder on + your computer rather than our specific website project folder. +- **The Fix:** Move into the correct directory before typing any git commands: + `cd path/to/scoutSite`. + +#### ❌ Error: `error: Your local changes to the following files would be overwritten by checkout...` + +- **What it means:** You modified some files on your computer while working on + the wrong branch, and Git is afraid it will erase your hard work if it + switches tasks. +- **The Fix:** Temporarily save your work in a hidden pocket, switch your + branch, and pull your work back out: + + ```bash + git stash + git checkout main + git pull origin main + git checkout -b your-new-branch-name + git stash pop + ``` diff --git a/wrangler.jsonc b/wrangler.jsonc index bf27353..dc6e1b7 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,14 +1,56 @@ { + // Links the file to an auto-validation schema so your code editor can catch typos "$schema": "node_modules/wrangler/config-schema.json", + + // The official unique identifying name for this Cloudflare Pages project deployment "name": "scouting331site", + + // Locked date ensuring Cloudflare preserves exact software feature behaviors from this day onward "compatibility_date": "2026-05-21", + + // Enables real-time server tracking logs to trace system performance and catch runtime errors "observability": { "enabled": true }, + + // Informs the Cloudflare engine where your compiled production website files live "assets": { "directory": "build" }, + + // Compatibility switches allowing our frontend server engine to run native Node.js commands "compatibility_flags": [ "nodejs_compat" - ] + ], + + // =========================================================================== + // GLOBAL PRODUCTION ENVIRONMENT SETTINGS + // =========================================================================== + "vars": { + // πŸš€ FORCES THE ACCELERATED LIVE MAIN WEBSITE TO COMPILE ON NODE 24 + "NODE_VERSION": "24" + }, + + // =========================================================================== + // ENVIRONMENT OVERRIDES FOR SITES AND PREVIEWS + // =========================================================================== + "env": { + // πŸ’‘ IMPORTANT FOR SCOUTS: These rules run ONLY on pull requests and branch preview drafts. + // They modify our development pipeline so that testing builds compile much faster. + "preview": { + "vars": { + // πŸš€ FORCES SCOUT DRAFT PREVIEW BUILD LINKS TO COMPILE ON NODE 24 TOO + "NODE_VERSION": "24", + + // Runs Docusaurus in raw developer compilation mode instead of heavy optimization mode + "NODE_ENV": "development", + + // Stops heavy Javascript compression and squeezing to save valuable build minutes + "DISABLE_MINIFICATION": "true", + + // Tells Docusaurus to skip making giant search engine maps for temporary test urls + "SKIP_SITEMAP": "true" + } + } + } }
{header} @@ -84,10 +90,13 @@ export default function CsvTable({ csvUrl }) {
+ {/* Safety fallback checking field elements for empty null values to display empty spaces instead */} {row[header] !== undefined && row[header] !== null ? row[header] : ""}