Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cf237d7
Add News Django app with model and settings registration
Noah4649 Jun 24, 2026
2590053
Merge branch 'main' of https://github.com/codersforcauses/electrify-e…
nicostellar Jun 27, 2026
8bb8671
Add news page and featured card components
bawn Jun 29, 2026
53cb3d0
Update featured-card.tsx
bawn Jul 1, 2026
5967dbb
Add news list
bawn Jul 1, 2026
7c0ed79
added full report pages for news. need to fix general css styling
phattydumpling Jul 3, 2026
4ae7b71
some css tweaks
bawn Jul 4, 2026
0cba8b4
Merge branch 'main' into issue-4-Create_News_Page
bawn Jul 7, 2026
5c932ec
use shared card component
bawn Jul 7, 2026
7c19e39
Add News API endpoints and serializer
bawn Jul 10, 2026
13c13ba
Add horizontal layout to ContentCard
bawn Jul 10, 2026
7f8c272
Integrate news API
bawn Jul 10, 2026
091480a
Delete news-card.tsx
bawn Jul 11, 2026
c4eaac4
Delete news-featured-card.tsx
bawn Jul 11, 2026
fbf0187
integrated backend for [id].tsx page for full reports. also added ima…
phattydumpling Jul 11, 2026
36889e2
modified css styling for news report pages
phattydumpling Jul 11, 2026
d852bd9
added image field and installed Pillow
phattydumpling Jul 11, 2026
094e6e1
modified news report page to correctly render image from backend. cur…
phattydumpling Jul 11, 2026
7999c14
modified main news page to render images
phattydumpling Jul 11, 2026
a81edff
fixed failed github actions - added newlines
phattydumpling Jul 15, 2026
b7753dc
updated dependencies file, adding Pillow for image upload
phattydumpling Jul 15, 2026
22a88ea
updated migration file to fix error on actions
phattydumpling Jul 15, 2026
68f5141
ran poetry lock as new dependencies were added
phattydumpling Jul 15, 2026
02a0989
updated Pillow version to match peotry.lock file
phattydumpling Jul 18, 2026
51da787
Merge branch 'main' into issue-4-Create_News_Page
phattydumpling Jul 18, 2026
512af64
added missed migrations causing api error
phattydumpling Jul 18, 2026
d9e8c3e
Merge branch 'issue-4-Create_News_Page' of https://github.com/codersf…
phattydumpling Jul 18, 2026
d25f807
Merge branch 'main' into issue-4-Create_News_Page
phattydumpling Jul 18, 2026
1161705
fixed dependency issues, fixed missing ',' causing wrong api import, …
phattydumpling Jul 18, 2026
abc1a09
fixed formatting issue
phattydumpling Jul 18, 2026
502915b
modified MEDIA_URL to NEWS_MEDIA_URL for definition - news images onyl
phattydumpling Jul 18, 2026
ca777d7
Merge branch 'main' into issue-4-Create_News_Page
phattydumpling Jul 18, 2026
5aea53e
modified MEDIA_URL to NEWS_MEDIA_URL for definition - news images onyl
phattydumpling Jul 18, 2026
3d86181
fixed media and media root paths for saving images
phattydumpling Jul 18, 2026
ee59690
updated urlpatterns for images on news page.
phattydumpling Jul 18, 2026
f7bcfb2
updating news image uploads to media/News
phattydumpling Jul 18, 2026
0f14a49
Merge branch 'main' into issue-4-Create_News_Page
phattydumpling Jul 18, 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
2,707 changes: 1,441 additions & 1,266 deletions client/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "^5.80.7",
"@tanstack/react-query": "^5.101.2",
"@tanstack/react-query-devtools": "^5.80.7",
"autoprefixer": "^10.4.21",
"axios": "^1.12.0",
Expand Down
Binary file added client/public/cover-test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion client/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ const buttonVariants = cva(
);

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
Expand Down
60 changes: 60 additions & 0 deletions client/src/hooks/news.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";

import api from "@/lib/api";

export interface NewsArticle {
id: string;
title: string;
slug: string;
summary: string | null;
body: string | null;
author: string | null;
published_at: string | null;
image: string;
}

const fetchNews = async () => {
const response = await api.get<NewsArticle[]>("/news/");

return response.data;
};

const fetchNewsArticle = async (id: string) => {
const response = await api.get<NewsArticle>(`/news/${id}/`);

return response.data;
};

export const useNews = (
args?: Omit<UseQueryOptions<NewsArticle[]>, "queryKey" | "queryFn">,
) => {
return useQuery({
...args,
queryKey: ["news"],
queryFn: fetchNews,
});
};

export const useNewsArticle = (
id?: string,
args?: Omit<UseQueryOptions<NewsArticle>, "queryKey" | "queryFn">,
) => {
return useQuery({
...args,
enabled: Boolean(id) && (args?.enabled ?? true),
queryKey: ["news", id],
queryFn: () => fetchNewsArticle(id as string),
});
};

export const formatPublishedDate = (publishedAt: string | null) => {
if (!publishedAt) {
return "Unpublished";
}

return new Intl.DateTimeFormat("en-AU", {
day: "numeric",
month: "long",
year: "numeric",
}).format(new Date(`${publishedAt}T00:00:00`));
};
3 changes: 3 additions & 0 deletions client/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Inter as FontSans } from "next/font/google";

Check warning on line 1 in client/src/pages/index.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint

Run autofix to sort these imports!

Check warning on line 1 in client/src/pages/index.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint

Run autofix to sort these imports!
import Link from "next/link";
import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";

Expand Down
58 changes: 58 additions & 0 deletions client/src/pages/news/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ArrowLeft } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";

import { useNewsArticle } from "@/hooks/news";

export default function ArticlePage() {
const router = useRouter();
const { id } = router.query;

const { data: article, isLoading, error } = useNewsArticle(id as string);

if (isLoading) {
return <p>Loading...</p>;
}

if (error || !article) {
return <p>Article not found.</p>;
}

return (
<div className="font-sans">
<header className="relative h-[514px] w-full overflow-hidden">
<Image
src={article.image || "/cover-test.jpg"} // Default image fallback
alt={article.title}
fill
className="object-cover"
priority
/>

<Link
href="/news"
className="absolute left-[98px] top-[65px] inline-flex items-center gap-2 text-lg font-medium text-gray-500 hover:text-white"
>
<ArrowLeft size={24} />
Back to News
</Link>

<div className="absolute left-[98px] top-[165px] z-10">
<h1 className="text-4xl font-bold text-white">{article.title}</h1>
</div>

<div className="absolute left-[98px] top-[413px] z-10 text-lg text-gray-300">
<span>{article.published_at}</span>
<span className="ml-5">By {article.author}</span>
</div>
</header>

<main className="px-10 py-8">
<div className="text-lg leading-relaxed text-gray-700">
<p>{article.body}</p>
</div>
</main>
</div>
);
}
67 changes: 67 additions & 0 deletions client/src/pages/news/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Inter as FontSans } from "next/font/google";

import { ContentCard } from "@/components/content-card";
import { formatPublishedDate, useNews } from "@/hooks/news";

import { NewsHero } from "./news-hero";
import { NewsList } from "./news-list";

const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});

export default function NewsPage() {
const { data: articles = [], isError, isLoading } = useNews();
const [featuredArticle, ...otherArticles] = articles;

return (
<>
<NewsHero />

{isLoading ? (
<section className="mx-auto max-w-7xl px-6 py-12">
<p className={`font-sans text-muted-foreground ${fontSans.variable}`}>
Loading news...
</p>
</section>
) : null}

{isError ? (
<section className="mx-auto max-w-7xl px-6 py-12">
<p className={`font-sans text-destructive ${fontSans.variable}`}>
News could not be loaded. Please try again later.
</p>
</section>
) : null}

{!isLoading && !isError && !featuredArticle ? (
<section className="mx-auto max-w-7xl px-6 py-12">
<p className={`font-sans text-muted-foreground ${fontSans.variable}`}>
No news articles have been published yet.
</p>
</section>
) : null}

{featuredArticle ? (
<article className="mx-auto max-w-7xl px-6 pt-12">
<ContentCard
className={`font-sans ${fontSans.variable}`}
key={featuredArticle.id}
imageSrc={featuredArticle.image || "/cover-test.jpg"}
imageAlt={featuredArticle.title}
title={featuredArticle.title}
description={featuredArticle.summary ?? ""}
href={`/news/${featuredArticle.id}`}
buttonLabel="Read full article"
author={featuredArticle.author ?? undefined}
dateTime={formatPublishedDate(featuredArticle.published_at)}
layout="horizontal"
/>
</article>
) : null}

{otherArticles.length > 0 ? <NewsList articles={otherArticles} /> : null}
</>
);
}
20 changes: 20 additions & 0 deletions client/src/pages/news/news-hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function NewsHero() {
return (
<section className="bg-amber-100">
<div className="mx-auto max-w-7xl px-6 py-16">
<p className="text-sm uppercase tracking-wider text-gray-600">
Latest from EEWA
</p>

<h1 className="mt-3 text-5xl font-bold text-gray-900">
News & Updates
</h1>

<p className="mt-6 max-w-2xl text-lg leading-8 text-gray-700">
Policy wins, adoption data, community programs, and analysis on
Washington State's clean energy transition.
</p>
</div>
</section>
);
}
36 changes: 36 additions & 0 deletions client/src/pages/news/news-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Inter as FontSans } from "next/font/google";

import { ContentCard } from "@/components/content-card";
import { formatPublishedDate, NewsArticle } from "@/hooks/news";

const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});

interface NewsListProps {
articles: NewsArticle[];
}

export function NewsList({ articles }: NewsListProps) {
return (
<section className="mx-auto max-w-7xl px-6 py-12">
<div className="grid gap-8 md:grid-cols-2 xl:grid-cols-3">
{articles.map((article) => (
<ContentCard
className={`font-sans ${fontSans.variable}`}
key={article.id}
imageSrc={article.image || "/cover-test.jpg"}
imageAlt={article.title}
title={article.title}
description={article.summary ?? ""}
href={`/news/${article.id}`}
buttonLabel="Read full article"
author={article.author ?? undefined}
dateTime={formatPublishedDate(article.published_at)}
/>
))}
</div>
</section>
);
}
Empty file added server/api/news/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions server/api/news/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.contrib import admin
from .models import News

# Register your models here.

admin.site.register(News)
6 changes: 6 additions & 0 deletions server/api/news/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class NewsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.news'
34 changes: 34 additions & 0 deletions server/api/news/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.1.14 on 2026-06-24 11:58

import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="News",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("title", models.CharField(max_length=255)),
("slug", models.SlugField(max_length=255, unique=True)),
("summary", models.TextField(blank=True)),
("body", models.TextField(blank=True)),
("author", models.CharField(blank=True, max_length=255)),
("published_at", models.DateField(blank=True, null=True)),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.16 on 2026-07-18 06:41

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("news", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="news",
name="image",
field=models.ImageField(blank=True, null=True, upload_to=""),
),
migrations.AlterField(
model_name="news",
name="published_at",
field=models.DateField(blank=True),
),
]
20 changes: 20 additions & 0 deletions server/api/news/migrations/0003_alter_news_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.2.16 on 2026-07-18 11:55

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("news", "0002_news_image_alter_news_published_at"),
]

operations = [
migrations.AlterField(
model_name="news",
name="image",
field=models.ImageField(
blank=True, default=None, null=True, upload_to="News"
),
),
]
Empty file.
18 changes: 18 additions & 0 deletions server/api/news/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import uuid
from django.db import models

# Create your models here.


class News(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
summary = models.TextField(blank=True)
body = models.TextField(blank=True)
author = models.CharField(max_length=255, blank=True)
published_at = models.DateField(blank=True)
image = models.ImageField(null=True, blank=True, upload_to="News", max_length=None, default=None)

def __str__(self):
return self.title
9 changes: 9 additions & 0 deletions server/api/news/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework import serializers
from .models import News


class NewsSerializer(serializers.ModelSerializer):
class Meta:
model = News
fields = "__all__"
# read_only_fields = ("id", "created_at")
3 changes: 3 additions & 0 deletions server/api/news/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# from django.test import TestCase

# Create your tests here.
Loading
Loading