From f929677fa64a3454fee4bd132a47e3244cd9f482 Mon Sep 17 00:00:00 2001 From: Shane-Donlon <130906067+Shane-Donlon@users.noreply.github.com> Date: Tue, 15 Oct 2024 07:29:30 +0100 Subject: [PATCH 1/6] style update add z-20 tailwind class to component (#1124) --- components/Nav/MobileNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Nav/MobileNav.tsx b/components/Nav/MobileNav.tsx index 6a7d6163..7a5d9bdc 100644 --- a/components/Nav/MobileNav.tsx +++ b/components/Nav/MobileNav.tsx @@ -25,7 +25,7 @@ const MobileNav: FunctionComponent = ({ close, }) => { return ( -
+
{navigation.map((item) => ( From 9803fd13cd2d6c764858322ff05016715cf2119b Mon Sep 17 00:00:00 2001 From: John <46611809+JohnAllenTech@users.noreply.github.com> Date: Tue, 15 Oct 2024 00:35:43 -0600 Subject: [PATCH 2/6] Feat/improving existing e2e tests (#1125) * chore: improved current E2E tests and enhanced labels * chore: added more tests around article page and unauth/auth views --- e2e/accessibility.spec.ts | 13 ++++++ e2e/articles.spec.ts | 95 ++++++++++++++++++++++++++++++++++++++- e2e/auth.setup.ts | 6 +-- e2e/home.spec.ts | 46 +++++++------------ e2e/login.spec.ts | 60 ++++++++++++++++++++----- 5 files changed, 174 insertions(+), 46 deletions(-) create mode 100644 e2e/accessibility.spec.ts diff --git a/e2e/accessibility.spec.ts b/e2e/accessibility.spec.ts new file mode 100644 index 00000000..15f3028a --- /dev/null +++ b/e2e/accessibility.spec.ts @@ -0,0 +1,13 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Accessibility Tests", () => { + test.describe("Confirm all images on homepage have alt text", () => { + test("Shared content", async ({ page }) => { + const imagesWithoutAltText = await page.$$eval( + "img:not([alt])", + (images) => images.length, + ); + expect(imagesWithoutAltText).toBe(0); // All images should have alt text + }); + }); +}); diff --git a/e2e/articles.spec.ts b/e2e/articles.spec.ts index 6b782ffc..bc153b67 100644 --- a/e2e/articles.spec.ts +++ b/e2e/articles.spec.ts @@ -1,6 +1,99 @@ import { test, expect } from "playwright/test"; -test.describe("Articles", () => { +test.describe("Unauthenticated Articles Page", () => { + test.beforeEach(async ({ page }) => { + await page.context().clearCookies(); + }); + + test("Should show popular tags", async ({ page, isMobile }) => { + await page.goto("http://localhost:3000/articles"); + await expect( + page.getByRole("heading", { name: "Popular topics" }), + ).toBeVisible({ visible: !isMobile }); + + await expect( + page.getByRole("link", { name: '"Codú Writing Challenge" text' }), + ).toBeVisible({ visible: !isMobile }); + }); + + test("Should not show bookmark article icon", async ({ page }) => { + await page.goto("http://localhost:3000/articles"); + + await expect( + page.getByRole("heading", { name: "Recent bookmarks" }), + ).toBeHidden(); + + await expect( + page.locator("article").first().getByLabel("Bookmark this post"), + ).toBeHidden(); + }); + test("Should load more articles when scrolling to the end of the page", async ({ + page, + isMobile, + }) => { + await page.goto("http://localhost:3000/articles"); + // Waits for articles to be loaded + await page.waitForSelector("article"); + + const initialArticleCount = await page.$$eval( + "article", + (articles) => articles.length, + ); + + if (!isMobile) { + await page.getByText("Code Of Conduct").scrollIntoViewIfNeeded(); + await page.waitForTimeout(5000); + const finalArticleCount = await page.$$eval( + "article", + (articles) => articles.length, + ); + expect(finalArticleCount).toBeGreaterThan(initialArticleCount); + } + + await expect(page.getByText("Home")).toBeVisible(); + await expect( + page.getByLabel("Footer").getByRole("link", { name: "Events" }), + ).toBeVisible(); + await expect(page.getByText("Sponsorship")).toBeVisible(); + await expect(page.getByText("Code Of Conduct")).toBeVisible(); + }); +}); + +test.describe("Authenticated Articles Page", () => { + test("Should show recent bookmarks", async ({ page, isMobile }) => { + await page.goto("http://localhost:3000/articles"); + await expect( + page.getByRole("heading", { name: "Popular topics" }), + ).toBeVisible({ visible: !isMobile }); + + await expect( + page.getByRole("link", { name: '"Codú Writing Challenge" text' }), + ).toBeVisible({ visible: !isMobile }); + + await expect( + page.getByRole("heading", { name: "Recent bookmarks" }), + ).toBeVisible({ visible: !isMobile }); + }); + + test("Should show bookmark article icon", async ({ page, isMobile }) => { + await page.goto("http://localhost:3000/articles"); + await expect( + page.getByRole("heading", { name: "Popular topics" }), + ).toBeVisible({ visible: !isMobile }); + + await expect( + page.getByRole("link", { name: '"Codú Writing Challenge" text' }), + ).toBeVisible({ visible: !isMobile }); + + await expect( + page.getByRole("heading", { name: "Recent bookmarks" }), + ).toBeVisible({ visible: !isMobile }); + + await expect( + page.locator("article").first().getByLabel("Bookmark this post"), + ).toBeVisible(); + }); + test("Should load more articles when scrolling to the end of the page", async ({ page, isMobile, diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts index de63a20d..ad2347be 100644 --- a/e2e/auth.setup.ts +++ b/e2e/auth.setup.ts @@ -28,14 +28,12 @@ setup("authenticate", async ({ page }) => { } try { - //expect(process.env.E2E_USER_SESSION_ID).toBeDefined(); removing until I can get it all working. - - const E2E_USER_SESSION_ID = "df8a11f2-f20a-43d6-80a0-a213f1efedc1"; + expect(process.env.E2E_USER_SESSION_ID).toBeDefined(); await page.context().addCookies([ { name: "next-auth.session-token", - value: E2E_USER_SESSION_ID as string, + value: process.env.E2E_USER_SESSION_ID as string, domain: "localhost", path: "/", sameSite: "Lax", diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts index f8f50898..acbcfcf8 100644 --- a/e2e/home.spec.ts +++ b/e2e/home.spec.ts @@ -1,20 +1,33 @@ import { test, expect } from "@playwright/test"; -test.describe("Testing homepage views", () => { - test("Authenticated homepage view", async ({ page, isMobile }) => { +test.describe("Authenticated homepage", () => { + test("Homepage view", async ({ page, isMobile }) => { await page.goto("http://localhost:3000/"); await expect(page.locator("h1")).not.toContainText("Unwanted text"); - if (!isMobile) + const elementVisible = await page + .locator('text="Popular topics"') + .isVisible(); + + if (isMobile) { + expect(elementVisible).toBe(false); + } else { await expect( page.getByRole("link", { name: "Your Posts", }), ).toBeVisible(); + expect(elementVisible).toBe(true); + } }); - test("Unauthenticated homepage view", async ({ page }) => { +}); + +test.describe("Unauthenticated homepage", () => { + test.beforeEach(async ({ page }) => { await page.context().clearCookies(); + }); + test("Homepage view", async ({ page }) => { await page.goto("http://localhost:3000/"); await expect(page.locator("h1")).not.toContainText("Unwanted text"); @@ -25,29 +38,4 @@ test.describe("Testing homepage views", () => { "The free web developer community", ); }); - - test("Authenticated landing page view", async ({ page, isMobile }) => { - await page.goto("http://localhost:3000/"); - - const elementVisible = await page - .locator('text="Popular topics"') - .isVisible(); - - if (isMobile) { - expect(elementVisible).toBe(false); - } else { - expect(elementVisible).toBe(true); - } - }); - - test.describe("Confirm image accessibiliy content", () => { - test("Shared content", async ({ page }) => { - // Accessibility - const imagesWithoutAltText = await page.$$eval( - "img:not([alt])", - (images) => images.length, - ); - expect(imagesWithoutAltText).toBe(0); // All images should have alt text - }); - }); }); diff --git a/e2e/login.spec.ts b/e2e/login.spec.ts index 491c77c5..7c5626ec 100644 --- a/e2e/login.spec.ts +++ b/e2e/login.spec.ts @@ -1,23 +1,59 @@ import { test, expect } from "playwright/test"; import "dotenv/config"; -test.describe("Login Page", () => { - test("should display the welcome message", async ({ page }) => { - await page.goto("http://localhost:3000/get-started"); - const welcomeMessage = page.getByText("Sign in or create your accounttton"); - expect(welcomeMessage).toBeTruthy(); - }); - test("should display the Github login button", async ({ page }) => { +test.describe("Unauthenticated Login Page", () => { + test.beforeEach(async ({ page }) => { await page.context().clearCookies(); await page.goto("http://localhost:3000/get-started"); - await page.waitForTimeout(3000); + }); + test("Sign up page contains sign up links", async ({ page, isMobile }) => { + await expect(page.getByText("CodúBetaSign in or create")).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Sign in or create your account" }), + ).toBeVisible(); + await expect(page.getByRole("link", { name: "return home" })).toBeVisible(); + if (!isMobile) { + await expect( + page.getByRole("button", { name: "Sign up for free" }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Sign in", exact: true }), + ).toBeVisible(); + } + }); + test("Login page contains GitHub button", async ({ page }) => { await expect(page.getByTestId("github-login-button")).toBeVisible(); }); - test("should display the Gitlab login button", async ({ page }) => { - await page.context().clearCookies(); - await page.goto("http://localhost:3000/get-started"); - await page.waitForLoadState(); + test("Login page contains GitLab button", async ({ page }) => { await expect(page.getByTestId("gitlab-login-button")).toBeVisible(); }); }); + +test.describe("Authenticated Login Page", () => { + test("Sign up page contains sign up links", async ({ page, isMobile }) => { + // authenticated users are kicked back to the homepage if they try to go to /get-started + await page.goto("http://localhost:3000/get-started"); + expect(page.url()).toEqual("http://localhost:3000/"); + await expect(page.getByText("CodúBetaSign in or create")).toBeHidden(); + await expect( + page.getByRole("heading", { name: "Sign in or create your account" }), + ).toBeHidden(); + await expect(page.getByRole("link", { name: "return home" })).toBeHidden(); + if (!isMobile) { + await expect( + page.getByRole("button", { name: "Sign up for free" }), + ).toBeHidden(); + await expect( + page.getByRole("button", { name: "Sign in", exact: true }), + ).toBeHidden(); + } + }); + test("Login page contains GitHub button", async ({ page }) => { + await expect(page.getByTestId("github-login-button")).toBeHidden(); + }); + + test("Login page contains GitLab button", async ({ page }) => { + await expect(page.getByTestId("gitlab-login-button")).toBeHidden(); + }); +}); From a47e8d295968bce083021a2b1ecb95f8c6a4a940 Mon Sep 17 00:00:00 2001 From: Niall Maher Date: Tue, 15 Oct 2024 10:04:27 +0100 Subject: [PATCH 3/6] Fix image upload on articles (#1126) * Fix image upload on articles * Adds safe actions for actions * Uses actions instead of trpc --- app/(app)/create/[[...paramsArr]]/_client.tsx | 63 +++++++++++-------- app/actions/getUploadUrl.ts | 44 +++++++++++++ package-lock.json | 42 +++++++++++++ package.json | 1 + server/api/router/profile.ts | 3 +- server/lib/safeAction.ts | 15 +++++ 6 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 app/actions/getUploadUrl.ts create mode 100644 server/lib/safeAction.ts diff --git a/app/(app)/create/[[...paramsArr]]/_client.tsx b/app/(app)/create/[[...paramsArr]]/_client.tsx index e98654be..1bd2a9fd 100644 --- a/app/(app)/create/[[...paramsArr]]/_client.tsx +++ b/app/(app)/create/[[...paramsArr]]/_client.tsx @@ -29,9 +29,9 @@ import copy from "copy-to-clipboard"; import { PostStatus, getPostStatus, isValidScheduleTime } from "@/utils/post"; import { ImageUp, LoaderCircle } from "lucide-react"; import { uploadFile } from "@/utils/s3helpers"; -import { type Session } from "next-auth"; +import { getUploadUrl } from "@/app/actions/getUploadUrl"; -const Create = ({ session }: { session: Session }) => { +const Create = () => { const params = useParams(); const router = useRouter(); @@ -71,29 +71,40 @@ const Create = ({ session }: { session: Session }) => { const file = e.target.files[0]; const { size, type } = file; - await getUploadUrl( - { size, type, config: { kind: "uploads", userId: session.user?.id } }, - { - onError(error) { - setUploadStatus("error"); - if (error) return toast.error(error.message); - return toast.error( - "Something went wrong uploading the photo, please retry.", - ); - }, - async onSuccess(signedUrl) { - const { fileLocation } = await uploadFile(signedUrl, file); - if (!fileLocation) { - setUploadStatus("error"); - return toast.error( - "Something went wrong uploading the photo, please retry.", - ); - } - setUploadStatus("success"); - setUploadUrl(fileLocation); - }, - }, - ); + try { + const res = await getUploadUrl({ + size, + type, + uploadType: "uploads", + }); + + const signedUrl = res?.data; + + if (!signedUrl) { + setUploadStatus("error"); + return toast.error( + "Something went wrong uploading the photo, please retry.", + ); + } + + const { fileLocation } = await uploadFile(signedUrl, file); + if (!fileLocation) { + setUploadStatus("error"); + return toast.error( + "Something went wrong uploading the photo, please retry.", + ); + } + setUploadStatus("success"); + setUploadUrl(fileLocation); + } catch (error) { + setUploadStatus("error"); + toast.error( + error instanceof Error + ? error.message + : "An error occurred while uploading the image.", + ); + Sentry.captureException(error); + } } }; @@ -162,8 +173,6 @@ const Create = ({ session }: { session: Session }) => { }, ); - const { mutate: getUploadUrl } = api.event.getUploadUrl.useMutation(); - const PREVIEW_URL = `${process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://www.codu.co"}/draft/${postId}`; const UPLOADED_IMAGE_URL = `![Image description](${uploadUrl})`; diff --git a/app/actions/getUploadUrl.ts b/app/actions/getUploadUrl.ts new file mode 100644 index 00000000..ffdbf5b0 --- /dev/null +++ b/app/actions/getUploadUrl.ts @@ -0,0 +1,44 @@ +"use server"; + +import * as Sentry from "@sentry/nextjs"; +import { getPresignedUrl } from "@/server/common/getPresignedUrl"; +import { authActionClient } from "@/server/lib/safeAction"; + +import { z } from "zod"; + +const schema = z.object({ + type: z.string(), + size: z.number(), + uploadType: z.enum(["uploads", "user"]).default("uploads"), +}); + +export const getUploadUrl = authActionClient + .schema(schema) + .action(async ({ parsedInput, ctx }) => { + const { type, size, uploadType } = parsedInput; + const extension = type.split("/")[1]; + const acceptedFormats = ["jpg", "jpeg", "gif", "png", "webp"]; + + if (!acceptedFormats.includes(extension)) { + throw new Error( + `Invalid file. Accepted file formats: ${acceptedFormats.join(", ")}.`, + ); + } + + if (size > 1048576 * 10) { + throw new Error("Maximum file size 10mb"); + } + + try { + const response = await getPresignedUrl(type, size, { + kind: uploadType, + userId: ctx.user.id, + }); + + return response; + } catch (error) { + Sentry.captureException(error); + console.error("Error getting presigned URL:", error); + throw new Error("Failed to upload image."); + } + }); diff --git a/package-lock.json b/package-lock.json index 70e3720b..358ab319 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,7 @@ "nanoid": "^5.0.7", "next": "^14.2.10", "next-auth": "^4.24.7", + "next-safe-action": "^7.9.4", "next-themes": "^0.3.0", "nodemailer": "^6.9.14", "pg": "^8.12.0", @@ -15097,6 +15098,47 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/next-safe-action": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/next-safe-action/-/next-safe-action-7.9.4.tgz", + "integrity": "sha512-ZQtkLzaSaf+3vMAHS5G7U1nG/Nw1o++25br52J1dHiAGU0RbyE8erhoie7A5HsypmjbdTNMZotTvWLYt1PGeDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/TheEdoRan" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=ES9JRPSC66XKW" + } + ], + "engines": { + "node": ">=18.17" + }, + "peerDependencies": { + "@sinclair/typebox": ">= 0.33.3", + "next": ">= 14.0.0", + "react": ">= 18.2.0", + "react-dom": ">= 18.2.0", + "valibot": ">= 0.36.0", + "yup": ">= 1.0.0", + "zod": ">= 3.0.0" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", diff --git a/package.json b/package.json index ec1a81b1..db35068f 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "nanoid": "^5.0.7", "next": "^14.2.10", "next-auth": "^4.24.7", + "next-safe-action": "^7.9.4", "next-themes": "^0.3.0", "nodemailer": "^6.9.14", "pg": "^8.12.0", diff --git a/server/api/router/profile.ts b/server/api/router/profile.ts index 6c274302..ff09a25b 100644 --- a/server/api/router/profile.ts +++ b/server/api/router/profile.ts @@ -1,4 +1,4 @@ -import { user, emailChangeHistory } from "@/server/db/schema"; +import { user } from "@/server/db/schema"; import { saveSettingsSchema, getProfileSchema, @@ -20,7 +20,6 @@ import { emailTokenReqSchema } from "@/schema/token"; import { generateEmailToken, sendVerificationEmail } from "@/utils/emailToken"; import { TOKEN_EXPIRATION_TIME } from "@/config/constants"; import { emailChangeRequest } from "@/server/db/schema"; -import { z } from "zod"; export const profileRouter = createTRPCRouter({ edit: protectedProcedure diff --git a/server/lib/safeAction.ts b/server/lib/safeAction.ts new file mode 100644 index 00000000..eaa8089b --- /dev/null +++ b/server/lib/safeAction.ts @@ -0,0 +1,15 @@ +import "server-only"; + +import { createSafeActionClient } from "next-safe-action"; +import { getServerAuthSession } from "@/server/auth"; +export const actionClient = createSafeActionClient(); + +export const authActionClient = actionClient.use(async ({ next }) => { + const session = await getServerAuthSession(); + + if (!session || !session.user) { + throw new Error("Session invalid."); + } + + return next({ ctx: { user: session.user } }); +}); From f663233d0bd22e5003d3947626f9d0595c45bb1a Mon Sep 17 00:00:00 2001 From: Niall Maher Date: Tue, 15 Oct 2024 12:40:31 +0100 Subject: [PATCH 4/6] Add this weeks newsletter (#1127) --- .../page.mdx | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/(app)/letters/javascript-features-coming-your-way-nodejs-course-launch/page.mdx diff --git a/app/(app)/letters/javascript-features-coming-your-way-nodejs-course-launch/page.mdx b/app/(app)/letters/javascript-features-coming-your-way-nodejs-course-launch/page.mdx new file mode 100644 index 00000000..0010eb87 --- /dev/null +++ b/app/(app)/letters/javascript-features-coming-your-way-nodejs-course-launch/page.mdx @@ -0,0 +1,88 @@ +Tue, October 15th 2024 • Niall Maher + +# 👀 10+ JavaScript Features Coming Your Way + Node.js Course Launch + +This week, I'm going to start with a developer tip! ✌️ + +Boost your productivity by creating custom [Git aliases](https://git-scm.com/book/ms/v2/Git-Basics-Git-Aliases) for frequently used commands. For example, you can set up 'git st' as an alias for 'git status' by running: + +``` + +git config --global alias.st status + +``` + +This simple trick can save you countless keystrokes and streamline your workflow. Experiment with aliases for your most-used Git commands to find what works best. + +🖥️ **New Node.js Course** + +I got it finished! Check it out and let me know what you think. + +- [Introduction to Node.js](https://www.codu.co/articles/introduction-to-node-js-wcw5rwlz) +- [Setting Up Node.js](https://www.codu.co/articles/setting-up-node-js-2fe_wafm) +- [Node.js and Non-Blocking Operations](https://www.codu.co/articles/event-driven-architecture-node-js-and-non-blocking-operations-i65wflm3) +- [Node.js vs JS in the Browser](https://www.codu.co/articles/node-js-vs-js-in-the-browser-dj1e1ilg) +- [Building a Simple CLI Tool with Node.js](https://www.codu.co/articles/building-a-simple-cli-tool-with-node-js-dha19one) +- [Working with Modules in Node.js](https://www.codu.co/articles/working-with-modules-in-node-js-kfgeg-by) +- [Node.js - Helpful Built-in Modules](https://www.codu.co/articles/node-js-helpful-built-in-modules-feb7akxt) +- [Task Manager CLI in Node.js with fs and path](https://www.codu.co/articles/task-manager-cli-in-node-js-with-fs-and-path-lav2xua5) +- [Creating a Basic Web Server with Node.js](https://www.codu.co/articles/creating-a-basic-web-server-with-node-js-ufn4yk5k) +- [Introduction to Testing in Node.js with Jest](https://www.codu.co/articles/introduction-to-testing-in-node-js-with-jest-uul8acvm) + +## 📚 This Week's Picks + +**[Building a Custom Avatar Generator (3 min)](https://www.codu.co/articles/building-a-custom-avatar-generator-1pgduezr)** + +This guide explains how I created a custom avatar generator that produces unique, colorful avatars based on a user's name. + +**[8 Open-Source Tools to grow your app (3 min)](https://dev.to/tolgee_i18n/8-open-source-tools-to-grow-your-app-and-reach-new-markets-5036)** + +Explore eight fantastic open-source tools that can help you localize your app, connect with your audience, and ultimately grow your user base across the globe. + +**[Boo! (CodePen)](https://codepen.io/konstantindenerz/pen/dyxONQb)** + +I love creative coding, and this is starting ti get festive with Halloween around the corner. + +**[I've Been Using These 8 Core Linux Commands Wrong for Years (5 min)](https://www.howtogeek.com/ive-been-using-these-core-linux-commands-wrong-for-years/)** + +Bobby Jack shares some mistakes he has made and that you could be too. + +**[3 Career Principles that got me to Director at Google (9 min)](https://read.highgrowthengineer.com/p/3-career-principles-to-director-at-google)** + +What helped Chaitali Narla get five promotions in 10 years at Google + +**[TC39 Advances 10+ ECMAScript Proposals: Key Features to Watch (5 min)](https://socket.dev/blog/tc39-advances-10-ecmascript-proposals-key-features-to-watch)** + +What new features should we be keeping eyes on in the JS ecosystem? This article has a great list. + +**[Build a documented / type-safe API with hono, drizzle, zod, OpenAPI and scalar (video)](https://www.youtube.com/watch?v=sNh9PoM9sUE)** + +In this video, CJ shows how to use hono and @hono/zod-openapi to build a fully type-safe API documented with an OpenAPI specification, including interactive documentation with scalar. + +## 📖 Book of the Week + +**[Build: An Unorthodox Guide to Making Things Worth Making](https://amzn.to/4dNiv3q)** + +For developers seeking inspiration for building beyond code, "Build" offers a fantastic look at the creative process. Written by Tony Fadell, co-creator of the iPod and founder of Nest, this book provides valuable insights into product development, innovation, and entrepreneurship. Fadell's unorthodox approach challenges conventional wisdom, encouraging developers to think beyond technical skills and embrace a holistic view of building products that truly matter. With real-world examples and hard-earned lessons from his illustrious career, this guide is essential for any developer looking to expand their impact and create meaningful products. + +## 🛠️ Something Cool + +**[Cursor (The AI Code Editor)](https://www.cursor.com/)** + +Over the past two weeks, I've extensively tested Cursor and found it to be a better tool for solving coding problems than Claude, ChatGPT, or GitHub Copilot. Cursor's key advantage lies in its ability to access and understand the context of your entire project and its files. This contextual awareness enables Cursor to generate more relevant and practical code solutions more consistently than other AI coding assistants I've used. + +## 🔗 Quick Links + +- **Codú TikTok:** [https://www.tiktok.com/@codu.co](https://www.tiktok.com/@codu.co) +- **Hacktoberfest GitHub Issues:** [https://github.com/codu-code/codu/issues](https://github.com/codu-code/codu/issues) +- **Our YouTube channel:** [https://www.youtube.com/@codu](https://www.youtube.com/@codu) + +That's it for this week! + +**If you have any ideas or feedback, reply to this email.** + +Thanks, and stay awesome, + +Niall + +_Founder @ [Codú](https://www.codu.co/?ref=newsletter)_ From 37e1080e22b3af88eb1114fa125abbd7e247863a Mon Sep 17 00:00:00 2001 From: Anonymus2000 <54113952+Nil2000@users.noreply.github.com> Date: Wed, 16 Oct 2024 20:32:15 +0530 Subject: [PATCH 5/6] UI part done for posting jobs (#1106) * UI part done for posting jobs * UI updated according to settings page * Added flag for jobs-create-page --- app/(app)/jobs/create/_client.tsx | 267 ++++++++++++++++++++++++++ app/(app)/jobs/create/page.tsx | 7 + public/images/company_placeholder.png | Bin 0 -> 1767 bytes utils/flags.ts | 1 + 4 files changed, 275 insertions(+) create mode 100644 app/(app)/jobs/create/_client.tsx create mode 100644 app/(app)/jobs/create/page.tsx create mode 100644 public/images/company_placeholder.png diff --git a/app/(app)/jobs/create/_client.tsx b/app/(app)/jobs/create/_client.tsx new file mode 100644 index 00000000..d2aaa4f5 --- /dev/null +++ b/app/(app)/jobs/create/_client.tsx @@ -0,0 +1,267 @@ +"use client"; + +import { Button } from "@/components/ui-components/button"; +import { + Checkbox, + CheckboxField, + CheckboxGroup, +} from "@/components/ui-components/checkbox"; +import { Divider } from "@/components/ui-components/divider"; +import { Description, Field, Label } from "@/components/ui-components/fieldset"; +import { Heading, Subheading } from "@/components/ui-components/heading"; +import { Input } from "@/components/ui-components/input"; +import { + Radio, + RadioField, + RadioGroup, +} from "@/components/ui-components/radio"; +import { Strong, Text } from "@/components/ui-components/text"; +import { Textarea } from "@/components/ui-components/textarea"; +import { FEATURE_FLAGS, isFlagEnabled } from "@/utils/flags"; +import Image from "next/image"; +import { notFound } from "next/navigation"; +import React, { useRef, useState } from "react"; + +export default function Content() { + const flagEnabled = isFlagEnabled(FEATURE_FLAGS.JOBS); + const fileInputRef = useRef(null); + const [imgUrl, setImgUrl] = useState(null); + + if (!flagEnabled) { + notFound(); + } + + return ( +
+ Post a job + +
+
+ Company Logo + Square format is best +
+ +
+ Company Logo +
+ + {}} + className="hidden" + ref={fileInputRef} + /> + + JPG, GIF or PNG. 1MB max. + +
+
+
+
+ + + +
+
+ Company Name + This will be shown in the format you type it +
+ + + + {/* Add error part after validation here */} +
+ + + +
+
+ Job Title + The job title for the position that you are opening +
+ + + + {/* Add error part after validation here */} +
+ + + +
+
+ Job Description + In markdown format +
+ +