Skip to content

Commit 87038df

Browse files
authored
Merge branch 'codu-code:develop' into develop
2 parents e6883a5 + 94401a6 commit 87038df

File tree

13 files changed

+213
-87
lines changed

13 files changed

+213
-87
lines changed

E2E Overview.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
To run the end-to-end tests using Playwright, you need to configure your environment and follow these steps:
44

5-
6-
75
### Session and User Setup
86

97
First, you need to add your E2E test user to your locally running database. Do this by running the following script if you haven't already:
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
Tuesday, November 5th, 2024 • Niall Maher
2+
3+
# ⚡️ Better Image Loading + React Native's New Architecture
4+
5+
[Read online](https://www.codu.co/letters/is-your-domain-haunted)
6+
7+
My tip of the week is something I just learned about. A property called `fetchpriority` that drastically improved our Core Web Vitals. While most developers know about lazy loading, combining a few loading properties can make a big difference. You can further boost the priority of fetching an image using the `fetchpriority` property.
8+
9+
Here's what it looks like in action:​​​​​​​
10+
11+
```html
12+
rel="preload" as="image" href="hero.webp" fetchpriority="high" > src="hero.webp"
13+
loading="eager" fetchpriority="high" decoding="async" >
14+
```
15+
16+
Let's break down why this works so well:
17+
18+
- The `<link rel="preload">` tells browsers to start downloading the image immediately, even before they discover the tag while parsing HTML
19+
- `fetchpriority="high"` bumps the image to the top of the browser's resource queue, making it download before less critical resources
20+
- `loading="eager"` disables lazy loading for this specific image (crucial for above-the-fold content)
21+
- `decoding="async"` lets the browser optimize image decode timing without blocking other tasks
22+
23+
**Just remember:** use this pattern sparingly for critical above-the-fold images directly impacting LCP. For everything else, stick with regular lazy loading.
24+
25+
And a **Pro tip:** Combine this with the `srcset` attribute to handle responsive images, and you've got a bulletproof image-loading strategy. I wrote a little article on it this week. [Read it here](https://www.codu.co/articles/responsive-images-in-html-using-srcset-lyb6r6r0).
26+
27+
## 📚 This Week's Picks
28+
29+
**[Creating Custom VS Code Snippets for Faster Development](https://www.codu.co/articles/creating-custom-vs-code-snippets-for-faster-development-kgjv1ct)** (4 min)\
30+
Let's explore how to create custom snippets that will boost your productivity.
31+
32+
**[Introduction to C# and .NET](https://www.codu.co/articles/introduction-to-c-and-net-w0hc8gz3)** (4 min)\
33+
What is C#, and what is .NET? The first article in a series Adrián is writing to teach you C#.
34+
35+
**[Enhanced local IDE experience for AWS Lambda developers](https://aws.amazon.com/blogs/compute/introducing-an-enhanced-local-ide-experience-for-aws-lambda-developers/)** (8 min)\
36+
AWS Lambda is introducing an improved local IDE experience to simplify Lambda-based application development. The new features help developers to author, build, debug, test, and deploy Lambda applications more efficiently in their local IDE.
37+
38+
**[25 crazy software bugs explained](https://www.youtube.com/watch?v=Iq_r7IcNmUk)** (video)\
39+
Fireship dives into 25 crazy software bugs that changed the world.
40+
41+
**[React Native - New Architecture is here](https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here)** (22 min)\
42+
The 0.76 release blog post shared a list of significant changes in this version. It provides an overview of the New Architecture and how it shapes the future of React Native.
43+
44+
**[Lessons learned from a successful Rust rewrite](https://gaultier.github.io/blog/lessons_learned_from_a_successful_rust_rewrite.html)** (12 min)\
45+
A great deep dive into some important lessons when migrating a C++ codebase to Rust.
46+
47+
## 📖 Book of the Week
48+
49+
**[The Engineering Executive's Primer: Impactful Technical Leadership](https://amzn.to/4fj5bWf)**
50+
51+
This book is essential for technical leaders navigating the complexities of engineering management. Drawing from his extensive experience at companies like Stripe and Uber, Larson offers practical frameworks for scaling engineering organizations, developing talent, and driving technical strategy. What sets this book apart is its focused approach to the unique challenges faced by senior engineering leaders, from managing organizational design to balancing technical debt with business growth. Particularly valuable are the actionable insights on building effective engineering processes and fostering a culture of technical excellence.
52+
53+
Whether you're an engineering executive or aspiring to become one, this book provides the strategic toolkit needed for impactful technical leadership.
54+
55+
## 🛠️ Something Cool
56+
57+
**[Posthog](https://posthog.com/)**
58+
59+
This is not a sponsored post (I am mentioning it because I do see Posthog sponsors a lot of content). I've been using this tool again recently for a couple of projects, and it's just fantastic.
60+
61+
PostHog offers the full package---session recordings, feature flags, A/B testing, and product analytics. Whether you're running early experiments or want to systemize your product decisions, PostHog provides the data and tools needed to make data-driven choices. It has a very generous free tier (which I haven't been lucky enough to hit just yet with any of my side projects).
62+
63+
## 🔗 Quick Links
64+
65+
- [Codú TikTok](https://www.tiktok.com/@codu.co)
66+
- [Hacktoberfest GitHub Issues](https://github.com/codu-code/codu/issues)
67+
- [Our YouTube channel](https://www.youtube.com/@codu)
68+
- [Find us on Twitch](https://www.twitch.tv/codudotco)
69+
70+
**If you have any ideas or feedback, reply to this email.**
71+
72+
Thanks, and stay awesome,
73+
74+
Niall
75+
76+
Founder @ [Codú](https://www.codu.co/?ref=newsletter)

app/(editor)/create/[[...paramsArr]]/_client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ const Create = ({ session }: { session: Session | null }) => {
557557
type="button"
558558
className="relative flex w-full focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 active:hover:bg-neutral-50 disabled:opacity-50"
559559
>
560-
<div className="input-base flex max-w-full flex-1 overflow-hidden border text-left">
560+
<div className="flex w-full max-w-full flex-1 overflow-hidden border px-2 py-2 text-left text-black shadow-sm ring-offset-1 focus:border-pink-500 focus:outline-none focus:ring-2 focus:ring-neutral-300 disabled:opacity-50 dark:border-white dark:bg-black dark:text-white sm:text-sm">
561561
{PREVIEW_URL}
562562
</div>
563563
<div className="absolute bottom-0 right-0 top-0 w-[120px] border border-neutral-300 bg-white px-4 py-2 font-medium text-neutral-600 shadow-sm">

components/ArticleMenu/ArticleMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ const ArticleMenu = ({
150150
</div>
151151

152152
<button
153-
className="focus-style-rounded rounded-full p-1 hover:bg-neutral-300 dark:hover:bg-neutral-800 lg:mx-auto"
153+
className="rounded-full p-1 hover:bg-neutral-300 focus:outline-none focus:ring-white focus-visible:ring-2 focus-visible:ring-pink-600 focus-visible:ring-offset-pink-600 dark:hover:bg-neutral-800 lg:mx-auto"
154154
aria-label="bookmark-trigger"
155155
onClick={() => {
156156
if (!session) {

components/ArticlePreview/ArticlePreview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ const ArticlePreview: NextPage<Props> = ({
155155
<div className="flex gap-x-2">
156156
{showBookmark && (
157157
<button
158-
className="focus-style-rounded rounded-full p-2 hover:bg-neutral-300 dark:hover:bg-neutral-800 lg:mx-auto"
158+
className="rounded-full p-2 hover:bg-neutral-300 focus:outline-none focus:ring-white focus-visible:ring-2 focus-visible:ring-pink-600 focus-visible:ring-offset-pink-600 dark:hover:bg-neutral-800 lg:mx-auto"
159159
onClick={() => {
160160
if (!session) {
161161
return signIn();

components/Footer/Footer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,25 @@ const navigation = {
1919
social: [
2020
{
2121
name: "Twitter",
22+
customStyle: "hover:bg-twitter focus:bg-twitter",
2223
href: twitterUrl,
2324
icon: Twitter,
2425
},
2526
{
2627
name: "GitHub",
28+
customStyle: "hover:bg-github focus:bg-github",
2729
href: githubUrl,
2830
icon: Github,
2931
},
3032
{
3133
name: "Discord",
34+
customStyle: "hover:bg-discord focus:bg-discord",
3235
href: discordInviteUrl,
3336
icon: Discord,
3437
},
3538
{
3639
name: "Youtube",
40+
customStyle: "hover:bg-youtube focus:bg-youtube",
3741
href: youtubeUrl,
3842
icon: Youtube,
3943
},
@@ -77,7 +81,7 @@ const Footer = () => {
7781
href={item.href}
7882
target="_blank"
7983
rel="noopener noreferrer"
80-
className={`focus-style rounded-md p-1 transition-all duration-300 hover:scale-105 hover:text-white hover:brightness-110 focus:scale-105 focus:text-white focus:brightness-110 ${item.name.toLowerCase()}`}
84+
className={`focus-style rounded-md p-1 transition-all duration-300 hover:scale-105 hover:text-white hover:brightness-110 focus:scale-105 focus:text-white focus:brightness-110 ${item.customStyle.toLowerCase()}`}
8185
>
8286
<span className="sr-only">{item.name}</span>
8387
<item.icon className="h-6 w-6" aria-hidden="true" />

e2e/articles.spec.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test, expect } from "playwright/test";
22
import { randomUUID } from "crypto";
3-
import { loggedInAsUserOne } from "./utils";
3+
import { articleContent, articleExcerpt, loggedInAsUserOne } from "./utils";
44

55
test.describe("Unauthenticated Articles Page", () => {
66
test("Should show popular tags", async ({ page, isMobile }) => {
@@ -17,10 +17,10 @@ test.describe("Unauthenticated Articles Page", () => {
1717
test("Should be able to navigate directly to an article", async ({
1818
page,
1919
}) => {
20-
await page.goto("http://localhost:3000/articles/e2e-test-slug-eqj0ozor");
21-
await expect(page.getByText("Lorem ipsum dolor sit amet,")).toBeVisible();
20+
await page.goto("http://localhost:3000/articles/e2e-test-slug-published");
21+
await expect(page.getByText(articleExcerpt)).toBeVisible();
2222
await expect(
23-
page.getByRole("heading", { name: "Test Article" }),
23+
page.getByRole("heading", { name: "Published Article" }),
2424
).toBeVisible();
2525
await expect(
2626
page.getByRole("heading", { name: "Written by E2E Test User One" }),
@@ -223,8 +223,6 @@ test.describe("Authenticated Articles Page", () => {
223223
});
224224

225225
test("Should write and publish an article", async ({ page, isMobile }) => {
226-
const articleContent =
227-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vitae ipsum id metus vestibulum rutrum eget a diam. Integer eget vulputate risus, ac convallis nulla. Mauris sed augue nunc. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam congue posuere tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Ut ac augue non libero ullamcorper ornare. Ut commodo ligula vitae malesuada maximus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam sagittis justo non justo placerat, a dapibus sapien volutpat. Nullam ullamcorper sodales justo sed.";
228226
const articleTitle = "Lorem Ipsum";
229227
await page.goto("http://localhost:3000");
230228
// Waits for articles to be loaded
@@ -261,7 +259,7 @@ test.describe("Authenticated Articles Page", () => {
261259
/^http:\/\/localhost:3000\/articles\/lorem-ipsum-.*$/,
262260
);
263261

264-
await expect(page.getByText("Lorem ipsum dolor sit amet,")).toBeVisible();
262+
await expect(page.getByText(articleExcerpt)).toBeVisible();
265263
await expect(
266264
page.getByRole("heading", { name: "Lorem Ipsum" }),
267265
).toBeVisible();
@@ -291,14 +289,14 @@ test.describe("Authenticated Articles Page", () => {
291289
});
292290

293291
test("Should be able reply to a comment", async ({ page }) => {
294-
await page.goto("http://localhost:3000/articles/e2e-test-slug-eqj0ozor");
292+
await page.goto("http://localhost:3000/articles/e2e-test-slug-published");
295293
const numberOfCommentsIntially = await page
296294
.locator("div")
297295
.filter({ hasText: /^Thanks for the positive feedback!$/ })
298296
.count();
299-
await expect(page.getByText("Lorem ipsum dolor sit amet,")).toBeVisible();
297+
await expect(page.getByText(articleExcerpt)).toBeVisible();
300298
await expect(
301-
page.getByRole("heading", { name: "Test Article" }),
299+
page.getByRole("heading", { name: "Published Article" }),
302300
).toBeVisible();
303301
await expect(
304302
page.getByRole("heading", { name: "Written by E2E Test User One" }),

e2e/my-posts.spec.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,56 @@
1-
import test from "@playwright/test";
2-
import { loggedInAsUserOne } from "./utils";
1+
import test, { expect } from "@playwright/test";
2+
import { articleExcerpt, loggedInAsUserOne } from "./utils";
33

44
test.describe("Unauthenticated my-posts Page", () => {
5-
//
6-
// Replace with tests for unauthenticated users
5+
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
6+
page,
7+
}) => {
8+
await page.goto("http://localhost:3000/my-posts");
9+
await page.waitForURL("http://localhost:3000/get-started");
10+
expect(page.url()).toEqual("http://localhost:3000/get-started");
11+
});
712
});
813

914
test.describe("Authenticated my-posts Page", () => {
1015
test.beforeEach(async ({ page }) => {
1116
await loggedInAsUserOne(page);
1217
});
13-
//
14-
// Replace with tests for authenticated users
18+
19+
test("Tabs for different type of posts should be visible", async ({
20+
page,
21+
}) => {
22+
await page.goto("http://localhost:3000/my-posts");
23+
24+
await expect(page.getByRole("link", { name: "Drafts" })).toBeVisible();
25+
await expect(page.getByRole("link", { name: "Scheduled" })).toBeVisible();
26+
await expect(page.getByRole("link", { name: "Published" })).toBeVisible();
27+
});
28+
29+
test("Different article tabs should correctly display articles matching that type", async ({
30+
page,
31+
}) => {
32+
await page.goto("http://localhost:3000/my-posts");
33+
34+
await expect(page.getByRole("link", { name: "Drafts" })).toBeVisible();
35+
await expect(page.getByRole("link", { name: "Scheduled" })).toBeVisible();
36+
await expect(page.getByRole("link", { name: "Published" })).toBeVisible();
37+
38+
await page.getByRole("link", { name: "Drafts" }).click();
39+
await expect(
40+
page.getByRole("heading", { name: "Draft Article" }),
41+
).toBeVisible();
42+
await expect(page.getByText(articleExcerpt)).toBeVisible();
43+
44+
await page.getByRole("link", { name: "Scheduled" }).click();
45+
await expect(
46+
page.getByRole("heading", { name: "Scheduled Article" }),
47+
).toBeVisible();
48+
await expect(page.getByText(articleExcerpt)).toBeVisible();
49+
50+
await page.getByRole("link", { name: "Published" }).click();
51+
await expect(
52+
page.getByRole("heading", { name: "Published Article" }),
53+
).toBeVisible();
54+
await expect(page.getByText(articleExcerpt, { exact: true })).toBeVisible();
55+
});
1556
});

e2e/setup.ts

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import postgres from "postgres";
22
import { drizzle } from "drizzle-orm/postgres-js";
33
import { post, comment, session, user } from "@/server/db/schema";
44
import {
5+
articleContent,
6+
articleExcerpt,
57
E2E_USER_ONE_EMAIL,
68
E2E_USER_ONE_ID,
79
E2E_USER_ONE_SESSION_ID,
@@ -12,37 +14,81 @@ import {
1214
import { eq } from "drizzle-orm";
1315

1416
export const setup = async () => {
17+
// Dynamically import nanoid
18+
const { nanoid } = await import("nanoid");
19+
1520
const db = drizzle(
1621
postgres("postgresql://postgres:[email protected]:5432/postgres"),
1722
);
18-
1923
const addE2EArticleAndComment = async (
2024
authorId: string,
2125
commenterId: string,
2226
) => {
23-
const postId = "1nFnMmN5";
27+
const publishedPostId = nanoid(8);
28+
const scheduledPostId = nanoid(8);
29+
const draftPostId = nanoid(8);
2430
const now = new Date().toISOString();
25-
await db
26-
.insert(post)
27-
.values({
28-
id: postId,
29-
published: now,
30-
excerpt: "Lorem ipsum dolor sit amet",
31-
updatedAt: now,
32-
slug: "e2e-test-slug-eqj0ozor",
33-
likes: 10,
34-
readTimeMins: 3,
35-
title: "Test Article",
36-
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vitae ipsum id metus vestibulum rutrum eget a diam. Integer eget vulputate risus, ac convallis nulla. Mauris sed augue nunc. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam congue posuere tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Ut ac augue non libero ullamcorper ornare. Ut commodo ligula vitae malesuada maximus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam sagittis justo non justo placerat, a dapibus sapien volutpat. Nullam ullamcorper sodales justo sed.",
37-
userId: authorId,
38-
})
39-
.onConflictDoNothing()
40-
.returning();
31+
32+
const oneYearFromToday = new Date(now);
33+
oneYearFromToday.setFullYear(oneYearFromToday.getFullYear() + 1);
34+
35+
await Promise.all([
36+
db
37+
.insert(post)
38+
.values({
39+
id: publishedPostId,
40+
published: now,
41+
excerpt: articleExcerpt,
42+
updatedAt: now,
43+
slug: "e2e-test-slug-published",
44+
likes: 10,
45+
readTimeMins: 3,
46+
title: "Published Article",
47+
body: articleContent,
48+
userId: authorId,
49+
})
50+
.onConflictDoNothing()
51+
.returning(),
52+
53+
db
54+
.insert(post)
55+
.values({
56+
id: draftPostId,
57+
published: null,
58+
excerpt: articleExcerpt,
59+
updatedAt: now,
60+
slug: "e2e-test-slug-draft",
61+
likes: 10,
62+
readTimeMins: 3,
63+
title: "Draft Article",
64+
body: articleContent,
65+
userId: authorId,
66+
})
67+
.onConflictDoNothing()
68+
.returning(),
69+
70+
db
71+
.insert(post)
72+
.values({
73+
id: scheduledPostId,
74+
published: oneYearFromToday.toISOString(),
75+
excerpt: articleExcerpt,
76+
updatedAt: now,
77+
slug: "e2e-test-slug-scheduled",
78+
likes: 10,
79+
readTimeMins: 3,
80+
title: "Scheduled Article",
81+
body: articleContent,
82+
userId: authorId,
83+
})
84+
.onConflictDoNothing()
85+
.returning(),
86+
]);
4187

4288
await db
4389
.insert(comment)
4490
.values({
45-
postId,
91+
postId: publishedPostId,
4692
body: "What a great article! Thanks for sharing",
4793
userId: commenterId,
4894
})

e2e/utils/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const articleContent =
2+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vitae ipsum id metus vestibulum rutrum eget a diam. Integer eget vulputate risus, ac convallis nulla. Mauris sed augue nunc. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam congue posuere tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Ut ac augue non libero ullamcorper ornare. Ut commodo ligula vitae malesuada maximus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam sagittis justo non justo placerat, a dapibus sapien volutpat. Nullam ullamcorper sodales justo sed.";
3+
4+
export const articleExcerpt = "Lorem ipsum dolor sit amet";

0 commit comments

Comments
 (0)