Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c4a968b
add resources table migration
fatmahussein Jun 16, 2026
edd68a7
add resources page endpoint
fatmahussein Jun 16, 2026
f2655ff
implement resources management
fatmahussein Jun 16, 2026
68d2ac0
add public API for listing and creating resources
fatmahussein Jun 16, 2026
8af5864
Add Resource model logic and validations
fatmahussein Jun 16, 2026
398bd57
restrict resource actions to admin users
fatmahussein Jun 16, 2026
a5c1a70
add client methods for listing and submitting resources
fatmahussein Jun 16, 2026
9ed5cae
add form for creating and editing resources
fatmahussein Jun 16, 2026
9647fd2
Add edit resource view
fatmahussein Jun 16, 2026
39e7622
add resources index view with management table
fatmahussein Jun 16, 2026
63cfbf6
add new resource view
fatmahussein Jun 16, 2026
dd5259c
add JS and CSS packs for resources page
fatmahussein Jun 16, 2026
986f089
Add resources link to admin navigation
fatmahussein Jun 16, 2026
7527caf
add reusable Modal component
fatmahussein Jun 16, 2026
63b6ee2
add resources link to footer navigation
fatmahussein Jun 16, 2026
5095d11
Add resources link to header navigation
fatmahussein Jun 16, 2026
63d4423
implement full resources page with listing and submission
fatmahussein Jun 16, 2026
30c7b14
Add resources page entry point
fatmahussein Jun 16, 2026
f4d7e38
adjust navbar spacing
fatmahussein Jun 16, 2026
b78bed1
Update loading dot color to match website theme
fatmahussein Jun 16, 2026
9d247a7
add modal component styles
fatmahussein Jun 16, 2026
625c756
Add resources page styles
fatmahussein Jun 16, 2026
0669aae
replace null_session with exception CSRF strategy
fatmahussein Jun 16, 2026
1d9a296
add FactoryBot resource factory
fatmahussein Jun 16, 2026
c35171d
add Resource model test coverage
fatmahussein Jun 16, 2026
2fc2c0d
Add system tests for admin resource management
fatmahussein Jun 16, 2026
2732291
Extend site page system specs to cover new resources feature
fatmahussein Jun 16, 2026
b550077
fix rubocop offenses
fatmahussein Jun 16, 2026
4b1a4f7
fix eslint offenses
fatmahussein Jun 16, 2026
c9bb479
email resource suggestions instead of saving them
fatmahussein Jun 30, 2026
2c3f7ea
use GMAIL_USERNAME as default mailer sender
fatmahussein Jun 30, 2026
c0ce048
add ResourceMailer for resource suggestions
fatmahussein Jun 30, 2026
7050e54
add HTML template for resource suggestion emails
fatmahussein Jun 30, 2026
741b69a
require submitter name for resource suggestions
fatmahussein Jun 30, 2026
01f1143
use organizers email for job posting inquiries
fatmahussein Jun 30, 2026
acad715
fix eslint offenses
fatmahussein Jun 30, 2026
f936ab3
fix eslint offense
fatmahussein Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/controllers/admin/resources_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module Admin
class ResourcesController < AdminController
include Pagy::Backend

before_action :authorize_resource
before_action :set_resource, only: %w[edit update destroy]

def index
@pagy, @resources = pagy(Resource.recent, items: 15)
end

def new
@resource = Resource.new
end

def create
@resource = Resource.new(resource_params)

if @resource.save
redirect_to admin_resources_path, notice: "Resource created successfully."
else
render :new, status: :unprocessable_content
end
end

def edit; end

def update
if @resource.update(resource_params)
redirect_to admin_resources_path, notice: "Resource updated successfully."
else
render :edit, status: :unprocessable_content
end
end

def destroy
@resource.destroy
redirect_to admin_resources_path, notice: "Resource deleted."
end

private

def authorize_resource
authorize Resource
end

def set_resource
@resource = Resource.find(params[:id])
end

def resource_params
params.require(:resource).permit(:title, :url, :description, :category, :submitted_by)
end
end
end
29 changes: 29 additions & 0 deletions app/controllers/api/resources_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Api
class ResourcesController < ApplicationController
protect_from_forgery with: :null_session

Check failure

Code scanning / CodeQL

CSRF protection weakened or disabled High

Potential CSRF vulnerability due to forgery protection being disabled or weakened.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

def index
resources = Resource.recent
resources = resources.by_category(params[:category]) if params[:category].present?
render json: { data: resources.map(&:as_json) }
end

def create
resource = Resource.new(resource_params)

if resource.save
render json: { message: "Resource submitted successfully!" }, status: :created
else
render json: { errors: resource.errors.full_messages }, status: :unprocessable_entity
end
end

private

def resource_params
params.permit(:title, :url, :description, :category, :submitted_by)
end
end
end
2 changes: 2 additions & 0 deletions app/controllers/site_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ def past_meetup; end
def sponsor_us; end

def community; end

def resources; end
end
45 changes: 45 additions & 0 deletions app/javascript/react/components/Modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';

import '../stylesheets/modal';

const Modal = ({ isOpen, onClose, children }) => {
const handleKeyDown = useCallback(
(e) => {
if (e.key === 'Escape') onClose();
},
[onClose],
);

useEffect(() => {
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [isOpen, handleKeyDown]);

if (!isOpen) return null;

return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" type="button" onClick={onClose} aria-label="Close">
&times;
</button>
{children}
</div>
</div>
);
};

Modal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
children: PropTypes.node,
};

export default Modal;
3 changes: 3 additions & 0 deletions app/javascript/react/components/layout/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ const Footer = () => (
<a className="footer-col-item" href="/meetups">
Meetups
</a>
<a className="footer-col-item" href="/resources">
Community Resources
</a>
</div>
</div>

Expand Down
7 changes: 4 additions & 3 deletions app/javascript/react/components/layout/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ const Header = () => {
const links = [
{ id: 1, text: 'Events', href: '/meetups' },
{ id: 2, text: 'Our Community', href: '/community' },
{ id: 3, text: 'Donate', href: 'https://buy.stripe.com/6oE7t874ReRc7gA9AN' },
{ id: 4, text: 'Support us', href: '/sponsor-us' },
{ id: 5, text: 'Join', href: '/join-us' },
{ id: 3, text: 'Resources', href: '/resources' },
{ id: 4, text: 'Donate', href: 'https://buy.stripe.com/6oE7t874ReRc7gA9AN' },
{ id: 5, text: 'Support us', href: '/sponsor-us' },
{ id: 6, text: 'Join', href: '/join-us' },
];

const [headerState, setHeaderState] = useState({
Expand Down
Loading
Loading