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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/stylesheets/views.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
@import 'views/mod-center';
@import 'views/signin';
@import 'views/signup-modal';
@import 'views/events';
226 changes: 226 additions & 0 deletions app/assets/stylesheets/views/events.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
.event-show-layout {
padding-top: var(--su-4);

.event-header {
display: flex;
align-items: center;
gap: var(--su-4);
margin-bottom: var(--su-4);

h1 {
font-size: var(--fs-3xl);
font-weight: 800;
color: var(--base-100);
line-height: var(--lh-tight);
margin: 0;
}

.live-indicator {
background-color: var(--error);
color: white;
padding: var(--su-1) var(--su-2);
border-radius: var(--radius);
font-weight: bold;
font-size: var(--fs-s);
letter-spacing: 0.5px;
animation: pulse 2s infinite;
}
}

.stream-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--su-4);
background: var(--base-0);
border-radius: var(--radius);
padding: var(--su-4);
box-shadow: var(--shadow-sm);

@media screen and (min-width: 992px) {
grid-template-columns: 3fr 1fr;
}

.video-wrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
border-radius: var(--radius);
background: var(--base-10);

iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: var(--radius);
}
}

.chat-wrapper {
height: 400px;
border-radius: var(--radius);
overflow: hidden;
background: var(--base-10);

@media screen and (min-width: 992px) {
height: 100%;
}

iframe {
width: 100%;
height: 100%;
border-radius: var(--radius);
}
}
}

.info-section {
display: flex;
flex-direction: column;
gap: var(--su-4);

p {
color: var(--base-70);
font-size: var(--fs-l);
line-height: var(--lh-relaxed);
}

.profile-card {
display: inline-flex;
align-items: center;
gap: var(--su-3);
padding: var(--su-3);
border: 1px solid var(--base-20);
border-radius: var(--radius);
text-decoration: none;
color: var(--base-90);
background: var(--base-0);
transition: all 0.2s ease;
align-self: flex-start;

&:hover {
background: var(--base-5);
border-color: var(--base-40);
box-shadow: var(--shadow-sm);
}

img {
width: 48px;
height: 48px;
border-radius: var(--radius-full);
}

div {
display: flex;
flex-direction: column;

strong {
color: var(--base-100);
}

span {
color: var(--base-60);
font-size: var(--fs-s);
}
}
}
}

.community-header {
h2 {
font-size: var(--fs-2xl);
}
}

.feed-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--su-4);

@media screen and (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}

@media screen and (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
}

.article-card {
background: var(--base-0);
border-radius: var(--radius-md);
padding: var(--su-4);
box-shadow: 0 0 0 1px var(--base-20);
display: flex;
flex-direction: column;
gap: var(--su-3);
transition: box-shadow 0.2s ease;

&:hover {
box-shadow: 0 0 0 1px var(--base-20), var(--shadow-sm);
}

.article-user {
display: flex;
align-items: center;
gap: var(--su-2);

img {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
}

span {
font-size: var(--fs-s);
color: var(--base-70);
font-weight: 500;
}
}

a {
text-decoration: none;
color: var(--base-100);

&:hover {
color: var(--link-color);
}

.article-title {
font-size: var(--fs-l);
font-weight: 700;
line-height: var(--lh-tight);
margin: 0;
}
}

.article-tags {
display: flex;
flex-wrap: wrap;
gap: var(--su-2);

.tag {
font-size: var(--fs-xs);
color: var(--base-60);
background: var(--base-10);
padding: 2px var(--su-2);
border-radius: var(--radius);
}
}

.article-meta {
margin-top: auto;
font-size: var(--fs-xs);
color: var(--base-60);
padding-top: var(--su-2);
}
}
}
}

@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
61 changes: 61 additions & 0 deletions app/controllers/admin/events_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module Admin
class EventsController < Admin::ApplicationController
before_action :set_event, only: %i[edit update destroy]

def index
@events = Event.all.order(created_at: :desc)
end

def new
@event = Event.new
end

def create
@event = Event.new(event_params)
if @event.save
redirect_to admin_events_path, notice: "Event created successfully."
else
render :new, status: :unprocessable_entity
end
end

def edit; end

def update
if @event.update(event_params)
redirect_to admin_events_path, notice: "Event updated successfully."
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@event.destroy
redirect_to admin_events_path, notice: "Event destroyed successfully."
end

private

def set_event
@event = Event.find(params[:id])
end

def event_params
params.require(:event).permit(
:title,
:event_name_slug,
:event_variation_slug,
:description,
:primary_stream_url,
:published,
:start_time,
:end_time,
:type_of,
:user_id,
:organization_id,
:tag_list,
data: {}
)
end
end
end
90 changes: 90 additions & 0 deletions app/controllers/api/v0/events_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module Api
module V0
class EventsController < ApiController
skip_before_action :verify_authenticity_token, only: %i[create update destroy]
before_action :authenticate!, except: %i[index show]
before_action :set_event, only: %i[show update destroy]

# Authentication is optional for index and show
# We manually attempt to authenticate to populate current_user if the token is present
before_action :evaluate_authentication, only: %i[index show]

def index
@events = Event.all
unless @user&.administrative_access_to?(resource: Event)
@events = @events.published
end
render json: @events.order(created_at: :desc)
end

def show
unless @event.published? || @user&.administrative_access_to?(resource: Event)
return render json: { error: "Event not found" }, status: :not_found
end
render json: @event
end

def create
authorize Event
@event = Event.new(event_params.except(:user_id))
@event.user_id = @user.id if @event.user_id.blank?

if @event.save
render json: @event, status: :created
else
render json: { error: @event.errors.full_messages }, status: :unprocessable_entity
end
end

def update
authorize @event
# Prevents arbitrary user hijacking via parameters:
if @event.update(event_params.except(:user_id))
render json: @event
else
render json: { error: @event.errors.full_messages }, status: :unprocessable_entity
end
end

def destroy
authorize @event
@event.destroy
head :no_content
end

private

def evaluate_authentication
# Forem's ApiController usually requires valid token if provided, but optional if omitted.
# This safely tries to log them in if token is sent.
if request.headers["api-key"]
authenticate!
end
end

def set_event
@event = Event.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "Event not found" }, status: :not_found
end

def event_params
params.require(:event).permit(
:title,
:event_name_slug,
:event_variation_slug,
:description,
:primary_stream_url,
:published,
:start_time,
:end_time,
:type_of,
:user_id,
:organization_id,
:tag_list,
data: {}
)
end
end
end
end
Loading
Loading