├── .env.example
├── .eslintrc.cjs
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jsLinters
│ └── eslint.xml
├── modules.xml
├── prettier.xml
├── sweprojects.iml
└── vcs.xml
├── README.md
├── assets
├── SWEProjects-logo-large.png
├── SWEProjects-logo-only.png
└── SWEProjects-logo-small.png
├── emails
├── ProjectPurchaseEmail.tsx
└── WelcomeEmail.tsx
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── prettier.config.cjs
├── prisma
└── schema.prisma
├── projects
├── FlappyBirdClone
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── next.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── api
│ │ │ └── scores.ts
│ │ └── index.tsx
│ ├── postcss.config.js
│ ├── public
│ │ ├── favicon.ico
│ │ ├── next.svg
│ │ └── vercel.svg
│ ├── styles
│ │ └── globals.css
│ ├── tailwind.config.js
│ └── tsconfig.json
└── LinkTreeClone
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── Components
│ ├── Account
│ │ └── UsernameEditor.tsx
│ ├── Setup
│ │ ├── LinksPreviewComponent.tsx
│ │ └── LinksSetupComponent.tsx
│ └── common
│ │ ├── BackgroundGradient.tsx
│ │ └── Header.tsx
│ ├── README.md
│ ├── next.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── pages
│ ├── [username].tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── account.tsx
│ ├── api
│ │ ├── links.ts
│ │ ├── usernames.ts
│ │ └── users.ts
│ ├── auth.tsx
│ ├── index.tsx
│ └── setup.tsx
│ ├── postcss.config.js
│ ├── public
│ ├── favicon.ico
│ ├── next.svg
│ └── vercel.svg
│ ├── styles
│ └── globals.css
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── types
│ └── supabase.ts
├── public
└── favicon.ico
├── src
├── components
│ ├── Common
│ │ ├── BackgroundGradient.tsx
│ │ ├── Header.tsx
│ │ ├── LoadingSpinner.tsx
│ │ ├── MdEditor
│ │ │ ├── MdEditor.tsx
│ │ │ └── utils.tsx
│ │ ├── PhotoUploadComponent.tsx
│ │ └── Selectors.tsx
│ ├── DraftProjects
│ │ ├── DraftCodeBlocks.tsx
│ │ ├── DraftInstructionalTextComponent.tsx
│ │ ├── DraftPageComponent.tsx
│ │ ├── Editor.tsx
│ │ ├── FocusedQuestion.tsx
│ │ └── InstructionLeftSidebar.tsx
│ ├── Images
│ │ ├── Celebration.tsx
│ │ ├── EmptyCart.tsx
│ │ ├── EmptyComments.tsx
│ │ ├── EndOfTheRoad.tsx
│ │ ├── PersonCoding.tsx
│ │ └── Questions.tsx
│ ├── LandingPage
│ │ ├── AuthLayout.tsx
│ │ ├── Button.tsx
│ │ ├── CallToAction.tsx
│ │ ├── Container.tsx
│ │ ├── Faqs.tsx
│ │ ├── Fields.tsx
│ │ ├── Footer.tsx
│ │ ├── Header.tsx
│ │ ├── Hero.tsx
│ │ ├── Logo.tsx
│ │ ├── NavLink.tsx
│ │ ├── Pricing.tsx
│ │ ├── PrimaryFeatures.tsx
│ │ ├── SecondaryFeatures.tsx
│ │ ├── Testimonials.tsx
│ │ └── images
│ │ │ ├── avatars
│ │ │ ├── avatar-1.png
│ │ │ ├── avatar-2.png
│ │ │ ├── avatar-3.png
│ │ │ ├── avatar-4.png
│ │ │ ├── avatar-5.png
│ │ │ └── avatar-6.png
│ │ │ ├── background-auth.jpg
│ │ │ ├── background-call-to-action.jpg
│ │ │ ├── background-faqs.jpg
│ │ │ ├── background-features.jpg
│ │ │ ├── logos
│ │ │ ├── laravel.svg
│ │ │ ├── mirage.svg
│ │ │ ├── statamic.svg
│ │ │ ├── statickit.svg
│ │ │ ├── transistor.svg
│ │ │ └── tuple.svg
│ │ │ └── screenshots
│ │ │ ├── canny-screenshot.png
│ │ │ ├── contacts.png
│ │ │ ├── expenses.png
│ │ │ ├── instructions-screenshot.png
│ │ │ ├── inventory.png
│ │ │ ├── payroll.png
│ │ │ ├── profit-loss.png
│ │ │ ├── reporting.png
│ │ │ └── vat-returns.png
│ ├── ProjectsV2
│ │ ├── CodeBlocks.tsx
│ │ ├── CodeDiffSection.tsx
│ │ ├── CompletedTutorial.tsx
│ │ ├── InstructionSidebar.tsx
│ │ ├── InstructionsToolbar.tsx
│ │ ├── ProjectTitleBlock.tsx
│ │ ├── PurchaseNudge.tsx
│ │ ├── QuestionsPurchaseNudge.tsx
│ │ ├── TableOfContentBlock.tsx
│ │ └── TextExplanationComponent.tsx
│ └── QuestionsAndAnswers
│ │ ├── CommentBox.tsx
│ │ ├── CommentsList.tsx
│ │ ├── QuestionAndAnswerComponent.tsx
│ │ ├── QuestionBox.tsx
│ │ └── QuestionsList.tsx
├── env.mjs
├── middleware.ts
├── pages
│ ├── _app.tsx
│ ├── api
│ │ ├── checkout_sessions.ts
│ │ ├── trpc
│ │ │ └── [trpc].ts
│ │ └── webhooks
│ │ │ ├── clerk
│ │ │ └── users.tsx
│ │ │ └── stripe.tsx
│ ├── index.tsx
│ ├── my-projects.tsx
│ ├── projects
│ │ ├── [username].tsx
│ │ ├── all.tsx
│ │ ├── index.tsx
│ │ ├── preview
│ │ │ └── [projectId].tsx
│ │ └── successfulPurchase.tsx
│ ├── projectsv2
│ │ └── [projectId]
│ │ │ ├── completed.tsx
│ │ │ └── index.tsx
│ ├── sign-in
│ │ └── [[...index]].tsx
│ └── sign-up
│ │ └── [[...index]].tsx
├── server
│ ├── api
│ │ ├── root.ts
│ │ ├── routers
│ │ │ ├── codeBlocks.ts
│ │ │ ├── comments.ts
│ │ │ ├── instructions.ts
│ │ │ ├── projectPreviewEnrollments.ts
│ │ │ ├── projects.ts
│ │ │ ├── purchases.ts
│ │ │ ├── questions.ts
│ │ │ └── stripe.ts
│ │ └── trpc.ts
│ ├── db.ts
│ └── helpers
│ │ └── ssgHelper.ts
├── styles
│ └── globals.css
└── utils
│ ├── api.ts
│ ├── types.ts
│ └── utils.ts
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to
2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date
3 | # when you add new variables to `.env`.
4 |
5 | # This file will be committed to version control, so make sure not to have any
6 | # secrets in it. If you are cloning this repo, create a copy of this file named
7 | # ".env" and populate it with your secrets.
8 |
9 | # When adding additional environment variables, the schema in "/src/env.mjs"
10 | # should be updated accordingly.
11 |
12 | # Prisma
13 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env
14 | DATABASE_URL="file:./db.sqlite"
15 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const path = require("path");
3 |
4 | /** @type {import("eslint").Linter.Config} */
5 | const config = {
6 | overrides: [
7 | {
8 | extends: [
9 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
10 | ],
11 | files: ["*.ts", "*.tsx"],
12 | parserOptions: {
13 | project: path.join(__dirname, "tsconfig.json"),
14 | },
15 | },
16 | ],
17 | parser: "@typescript-eslint/parser",
18 | parserOptions: {
19 | project: path.join(__dirname, "tsconfig.json"),
20 | },
21 | plugins: ["@typescript-eslint"],
22 | extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
23 | rules: {
24 | "@typescript-eslint/consistent-type-imports": [
25 | "warn",
26 | {
27 | prefer: "type-imports",
28 | fixStyle: "inline-type-imports",
29 | },
30 | ],
31 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
32 | },
33 | };
34 |
35 | module.exports = config;
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # database
12 | /prisma/db.sqlite
13 | /prisma/db.sqlite-journal
14 |
15 | # next.js
16 | /.next/
17 | /out/
18 | next-env.d.ts
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # local env files
34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
35 | .env
36 | .env*.local
37 |
38 | # vercel
39 | .vercel
40 |
41 | # typescript
42 | *.tsbuildinfo
43 | *.react-email/
44 | **/.idea/**
45 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/sweprojects.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | # SWE Projects
5 |
6 | SWE Projects is an open source project that aims to help people build projects that go beyond working on localhost.
7 | It aims to create high quality tutorials for high quality projects.
8 |
9 | [sweprojects.com](https://sweprojects.com)
10 |
11 |
12 | ## Why SWE Projects?
13 | SWE Projects was created to be the go-to destination of high quality coding projects and tutorials on how to build them.
14 |
15 | Most existing coding tutorials are random articles found on the internet or multiple hour long videos on Youtube that say a project is "complete" once it works on localhost.
16 |
17 | We want to change that by creating high quality tutorials for technically impressive projects that you can actually deploy out onto the internet and share with friends, families, recruiters.
18 |
19 |
20 | ## Get Started
21 | To run SWE Projects locally, just run the following commands to install the dependencies and run the app locally.
22 |
23 | ```
24 | npm install
25 |
26 | npm run dev
27 | ```
28 |
29 | ### Connect your own database
30 | SWE Projects uses Planetscale for our database. To connect your own local instance to SWE Projects, create a Planetsscale
31 | account and create a database. Once you create a database, you can connect it to SWE Projects by creating a `.env` file
32 | at the root directory and add an environment variable called `DATABASE_URL` pointing to your Planetscale db url.
33 |
34 | After adding your `DATABASE_URL`, run `npx prisma db push && npm install` to initialize the database on your local instance.
35 |
36 | You probably also want to set up authentication as well. SWE Projects uses Clerk for authentication. Create your Clerk account
37 | and pass in the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` env variables to set it all up.
38 |
39 | If you're confused, you should check out Theo's video [here](https://www.youtube.com/watch?v=YkOSUVzOAA4&t=7749s) on how to set up Planetscale and Clerk.
40 | SWE Project uses the t3 starter kit mentioned in the video so you should be able to follow along with the video.
41 |
42 | TODO(YourAverageTechBro): Provide test data script
43 |
44 | We are working on writing up some instructions on how to create your own database instance to develop locally.
45 |
46 | ## Is SWE Projects Free?
47 |
48 | We plan to include the source code for every project that we write a tutorial for. However, we do plan to charge for the step-by-step written tutorials at [sweprojects.com](https://sweprojects.com).
49 |
50 | ## Community & Support
51 |
52 |
53 | * [Discord](https://discord.gg/2p2e5tTmzw) — chat with the SWE Projects team and other developers
54 | * [Canny](https://sweprojects.canny.io/feature-requests) - Request/upvote feature requests and project requests
55 | * [GitHub issues](https://github.com/YourAverageTechBro/SWEProjects/issues/new) - to report bugs
56 |
57 | ## How You Can Contribute
58 | ### Submit a project + Write a Tutorial
59 |
60 | Have a project that you built that you want to write a tutorial for? Join our [Discord](https://discord.gg/2p2e5tTmzw) and
61 | post a message in the `#project-proposal` channel. If you create a project tutorial, we will split all sales of the
62 | project tutorial with you — a great way to make some extra income.
63 |
64 | ### Translate an existing project to another language
65 | We want to make sure that every project in SWE Projects is offered in a variety of languages and tech stacks.
66 | If you see a project that you want to translate into a different project, join our [Discord](https://discord.gg/2p2e5tTmzw)
67 | and post a message in the `#project-translation` channel.
68 |
69 |
--------------------------------------------------------------------------------
/assets/SWEProjects-logo-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/assets/SWEProjects-logo-large.png
--------------------------------------------------------------------------------
/assets/SWEProjects-logo-only.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/assets/SWEProjects-logo-only.png
--------------------------------------------------------------------------------
/assets/SWEProjects-logo-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/assets/SWEProjects-logo-small.png
--------------------------------------------------------------------------------
/emails/ProjectPurchaseEmail.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@react-email/link";
2 | import { Section } from "@react-email/section";
3 | import { Tailwind } from "@react-email/tailwind";
4 |
5 | type Props = {
6 | projectName: string;
7 | projectUrl: string;
8 | };
9 | const ProjectPurchaseEmail = ({ projectName, projectUrl }: Props) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
21 |
22 | Congrats on purchasing the {projectName} tutorial!
23 |
24 |
25 |
26 | Click on the button below to access the tutorial
27 |
28 |
29 |
33 | Start The {projectName} Tutorial
34 |
35 |
36 |
37 | If you run into any issues, stop by
38 |
42 | {" the Discord "}
43 |
44 | or send an email to dohyun@youraveragetechbro.com with details
45 | about your issue. We will help you out as soon as we can.
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | // Styles for the email template
55 | const main = {
56 | backgroundColor: "#ffffff",
57 | fontFamily:
58 | "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
59 | };
60 |
61 | export default ProjectPurchaseEmail;
62 |
--------------------------------------------------------------------------------
/emails/WelcomeEmail.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@react-email/link";
2 | import { Section } from "@react-email/section";
3 | import { Tailwind } from "@react-email/tailwind";
4 |
5 | const WelcomeEmail = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
17 |
Welcome to SWE Projects!
18 |
19 |
20 | Hi, this is Dohyun (also known as YourAverageTechBro on social
21 | media), the founder and creator of SWE Projects.
22 |
23 |
24 |
25 | I wanted to share a few helpful resources regarding SWE Projects.
26 |
27 |
28 |
29 | Sign up for
30 |
34 | {" email updates "}
35 |
36 | to get notified when new projects and features are released (we
37 | promise to not be annoying)
38 |
39 |
40 |
41 | Join
42 |
46 | {" the Discord "}
47 |
48 | if you are running into any issues/need help
49 |
50 |
51 |
52 | Leave project/feature requests on the
53 |
57 | {" SWE Projects Canny Board"}
58 |
59 |
60 |
61 |
62 |
63 | {
64 | "That's all for now — see you in the Discord and happy learning 🙂"
65 | }
66 |
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | // Styles for the email template
75 | const main = {
76 | backgroundColor: "#ffffff",
77 | fontFamily:
78 | "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
79 | };
80 |
81 | export default WelcomeEmail;
82 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
3 | * This is especially useful for Docker builds.
4 | */
5 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs"));
6 | import removeImports from "next-remove-imports";
7 |
8 |
9 | /** @type {import("next").NextConfig} */
10 | const config = {
11 | reactStrictMode: true,
12 |
13 | /**
14 | * If you have the "experimental: { appDir: true }" setting enabled, then you
15 | * must comment the below `i18n` config out.
16 | *
17 | * @see https://github.com/vercel/next.js/issues/41980
18 | */
19 | i18n: {
20 | locales: ["en"],
21 | defaultLocale: "en",
22 | },
23 | };
24 |
25 | /** @type {function(import("next").NextConfig): import("next").NextConfig}} */
26 | const removeImportsFun = removeImports({})
27 |
28 | export default removeImportsFun({
29 | webpack: function(config) {
30 | config.module.rules.push({
31 | test: /\.md$/,
32 | use: 'raw-loader',
33 | })
34 | return config
35 | },
36 | })
37 | // export default config;
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sweprojects",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "next build",
7 | "dev": "next dev",
8 | "email": "email dev",
9 | "postinstall": "prisma generate",
10 | "lint": "next lint",
11 | "start": "next start"
12 | },
13 | "dependencies": {
14 | "@blocknote/core": "^0.5.1",
15 | "@blocknote/react": "^0.5.1",
16 | "@clerk/backend": "^0.17.2",
17 | "@clerk/nextjs": "^4.15.0",
18 | "@headlessui/react": "^1.7.13",
19 | "@heroicons/react": "^2.0.17",
20 | "@mailerlite/mailerlite-nodejs": "^1.0.1",
21 | "@monaco-editor/react": "^4.5.0",
22 | "@react-email/button": "0.0.8",
23 | "@react-email/html": "0.0.4",
24 | "@react-email/link": "^0.0.5",
25 | "@react-email/section": "^0.0.9",
26 | "@react-email/tailwind": "^0.0.8",
27 | "@stripe/stripe-js": "^1.52.1",
28 | "@supabase/auth-helpers-nextjs": "^0.6.1",
29 | "@supabase/auth-helpers-react": "^0.3.1",
30 | "@supabase/auth-ui-react": "^0.3.5",
31 | "@supabase/supabase-js": "^2.19.0",
32 | "@tanstack/react-query": "^4.28.0",
33 | "@trpc/client": "^10.18.0",
34 | "@trpc/next": "^10.18.0",
35 | "@trpc/react-query": "^10.18.0",
36 | "@trpc/server": "^10.18.0",
37 | "@uiw/react-md-editor": "^3.20.8",
38 | "@vercel/analytics": "^0.1.11",
39 | "micro": "^10.0.1",
40 | "mixpanel-browser": "^2.47.0",
41 | "monaco-jsx-syntax-highlight": "^1.2.0",
42 | "next": "^13.3.0",
43 | "next-axiom": "^0.17.0",
44 | "next-remove-imports": "^1.0.11",
45 | "posthog-js": "^1.55.1",
46 | "posthog-node": "^3.1.1",
47 | "react": "18.2.0",
48 | "react-dom": "18.2.0",
49 | "react-email": "^1.9.3",
50 | "react-hot-toast": "^2.4.0",
51 | "react-images-uploading": "^3.1.7",
52 | "resend": "^0.14.0",
53 | "stripe": "^12.2.0",
54 | "superjson": "^1.12.2",
55 | "svix": "^0.84.1",
56 | "zod": "^3.21.4"
57 | },
58 | "devDependencies": {
59 | "@prisma/client": "^4.14.1",
60 | "@types/eslint": "^8.21.3",
61 | "@types/mixpanel-browser": "^2.38.1",
62 | "@types/node": "^18.15.5",
63 | "@types/prettier": "^2.7.2",
64 | "@types/react": "^18.0.28",
65 | "@types/react-dom": "^18.0.11",
66 | "@typescript-eslint/eslint-plugin": "^5.56.0",
67 | "@typescript-eslint/parser": "^5.56.0",
68 | "autoprefixer": "^10.4.14",
69 | "eslint": "^8.36.0",
70 | "eslint-config-next": "^13.2.4",
71 | "i": "^0.3.7",
72 | "npm": "^9.6.7",
73 | "postcss": "^8.4.21",
74 | "prettier": "^2.8.7",
75 | "prettier-plugin-tailwindcss": "^0.2.6",
76 | "prisma": "^4.14.1",
77 | "tailwindcss": "^3.3.0",
78 | "typescript": "^5.0.2"
79 | },
80 | "ct3aMetadata": {
81 | "initVersion": "7.10.1"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | const config = {
3 | plugins: [require.resolve("prettier-plugin-tailwindcss")],
4 | };
5 |
6 | module.exports = config;
7 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "mysql"
3 | url = env("DATABASE_URL")
4 | relationMode = "prisma"
5 | }
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | model Projects {
12 | id String @id @default(cuid())
13 | createdAt DateTime @default(now())
14 | authorId String
15 | projectVariants ProjectVariant[]
16 | thumbnailUrl String @default("")
17 | title String @default("Sample title") @db.LongText
18 | description String @default("An awesome description") @db.LongText
19 | videoDemoUrl String @default("")
20 | stripePriceId String @default("")
21 | purchases Purchases[]
22 | preRequisites String? @default("") @db.LongText
23 | projectAccessType ProjectAccessType? @default(Free)
24 | projectPreviewEnrollments ProjectPreviewEnrollment[]
25 | instructions Instructions[]
26 |
27 | @@index([authorId])
28 | }
29 |
30 | model Purchases {
31 | id String @id @default(cuid())
32 | createdAt DateTime @default(now())
33 | Projects Projects? @relation(fields: [projectsId], references: [id])
34 | projectsId String?
35 | userId String
36 | }
37 |
38 | model ProjectPreviewEnrollment {
39 | id String @id @default(cuid())
40 | createdAt DateTime @default(now())
41 | Projects Projects? @relation(fields: [projectsId], references: [id])
42 | projectsId String?
43 | userId String
44 | email String
45 | }
46 |
47 | model ProjectVariant {
48 | id String @id @default(cuid())
49 | createdAt DateTime @default(now())
50 | frontendVariant FrontendVariant
51 | backendVariant BackendVariant
52 | instructions Instructions[]
53 | Projects Projects? @relation(fields: [projectsId], references: [id])
54 | projectsId String?
55 | authorId String @default("")
56 | }
57 |
58 | model Instructions {
59 | id String @id @default(cuid())
60 | createdAt DateTime @default(now())
61 | explanation String @default("") @db.LongText
62 | ProjectVariant ProjectVariant? @relation(fields: [projectVariantId], references: [id])
63 | projectVariantId String?
64 | hasCodeBlocks Boolean @default(true)
65 | codeBlock CodeBlocks[]
66 | questions Questions[]
67 | title String @default("") @db.LongText
68 | Projects Projects? @relation(fields: [projectsId], references: [id])
69 | projectsId String?
70 | }
71 |
72 | model Questions {
73 | id String @id @default(cuid())
74 | createdAt DateTime @default(now())
75 | instructionsId String
76 | instructions Instructions? @relation(fields: [instructionsId], references: [id], onDelete: Cascade)
77 | userId String
78 | username String? @default("") @db.VarChar(255)
79 | title String @default("") @db.LongText
80 | question String @default("") @db.LongText
81 | comments Comment[]
82 | }
83 |
84 | model Comment {
85 | id String @id @default(cuid())
86 | createdAt DateTime @default(now())
87 | questionId String
88 | questions Questions? @relation(fields: [questionId], references: [id], onDelete: Cascade)
89 | userId String
90 | username String? @default("") @db.VarChar(255)
91 | comment String @default("") @db.LongText
92 | parentCommentId String?
93 | parentComment Comment? @relation("CommentThread", fields: [parentCommentId], references: [id], onDelete: NoAction, onUpdate: NoAction)
94 | replies Comment[] @relation("CommentThread")
95 | }
96 |
97 | model CodeBlocks {
98 | id String @id @default(cuid())
99 | createdAt DateTime @default(now())
100 | instructionsId String
101 | instructions Instructions? @relation(fields: [instructionsId], references: [id], onDelete: Cascade)
102 | code String @db.LongText
103 | fileName String
104 | }
105 |
106 | enum FrontendVariant {
107 | NextJS
108 | }
109 |
110 | enum BackendVariant {
111 | Supabase
112 | PlanetScale
113 | }
114 |
115 | enum ProjectAccessType {
116 | Free
117 | Paid
118 | Sponsored
119 | }
120 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 | .idea/
37 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/README.md:
--------------------------------------------------------------------------------
1 | # FlappyBirdClone
2 |
3 | This is a clone of the popular game Flappy Bird. This game is made using NextJS and Supabase.
4 |
5 | [Here](https://youtu.be/OqO05TfRE3A) is a demo of what the app looks like.
6 |
7 |
8 | ## How To Run Locally
9 |
10 | To run the game locally, run `npm install` to install all the necessary dependencies and then run `npm run dev` to run web appp.
11 |
12 | This iteration of Flappy bird stores the score of the user's game in a database.
13 | This project uses [Supabase](https://supabase.com/) as the database. To set it up, create a `.env` file at the
14 | root directory and include the following env variables.
15 |
16 | ```
17 | NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
18 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_project_anon_public_key
19 | ```
20 |
21 | You can find the values for the two env variables in your Supabase project settings in the `API` tab.
22 |
23 | Once you add your Supabase keys, you need to create the actual `scores` table in your Supabase project with the following schema:
24 |
25 |
26 | ```
27 | table scores {
28 | id: string
29 | created_at: string;
30 | score: number;
31 | }
32 | ```
33 |
34 | ## Want Step-By-Step Instructions?
35 |
36 | If you want a step-by-step tutorial on how to build this project, you can find the tutorial on [here](https://www.sweprojects.com/projects/preview/clgk8x5w1000cvrvb86b13ut7).
37 |
38 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-project",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@supabase/auth-helpers-nextjs": "^0.6.1",
13 | "@supabase/auth-helpers-react": "^0.3.1",
14 | "@supabase/supabase-js": "^2.20.0",
15 | "@types/node": "18.15.11",
16 | "@types/react": "18.0.35",
17 | "@types/react-dom": "18.0.11",
18 | "autoprefixer": "10.4.14",
19 | "eslint": "8.38.0",
20 | "eslint-config-next": "13.3.0",
21 | "next": "13.3.0",
22 | "postcss": "8.4.22",
23 | "react": "18.2.0",
24 | "react-dom": "18.2.0",
25 | "tailwindcss": "3.3.1",
26 | "typescript": "5.0.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css'
2 | import type { AppProps } from 'next/app'
3 |
4 | export default function App({ Component, pageProps }: AppProps) {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/pages/api/scores.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createServerSupabaseClient,
3 | SupabaseClient,
4 | } from "@supabase/auth-helpers-nextjs";
5 | import type { NextApiRequest, NextApiResponse } from "next";
6 |
7 | export default async function handler(
8 | req: NextApiRequest,
9 | res: NextApiResponse
10 | ) {
11 | try {
12 | const supabase = createServerSupabaseClient({ req, res });
13 | if (req.method === "POST") {
14 | const { score } = JSON.parse(req.body);
15 | await handlePost(supabase, score, res);
16 | } else if (req.method === "GET") {
17 | await handleGet(supabase, res);
18 | }
19 | } catch (error: any) {
20 | res.status(500).json({ name: error.message });
21 | }
22 | }
23 |
24 | const handlePost = async (
25 | supabase: SupabaseClient,
26 | score: number,
27 | res: NextApiResponse
28 | ) => {
29 | const { error } = await supabase.from("scores").insert({ score });
30 | if (error) throw error;
31 | res.status(200).send("OK");
32 | };
33 |
34 | const handleGet = async (supabase: SupabaseClient, res: NextApiResponse) => {
35 | const { data, error } = await supabase
36 | .from("scores")
37 | .select("score")
38 | .order("score", { ascending: false })
39 | .limit(10);
40 | if (error) throw error;
41 | res.status(200).json({ data: data.map((d) => d.score) });
42 | };
43 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/projects/FlappyBirdClone/public/favicon.ico
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './pages/**/*.{js,ts,jsx,tsx}',
5 | './components/**/*.{js,ts,jsx,tsx}',
6 | './app/**/*.{js,ts,jsx,tsx}',
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12 | 'gradient-conic':
13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | }
19 |
--------------------------------------------------------------------------------
/projects/FlappyBirdClone/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/*": ["./*"]
19 | }
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 | *.idea/
37 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/Components/Account/UsernameEditor.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useUser } from "@supabase/auth-helpers-react";
3 |
4 | type Props = {
5 | username: string;
6 | };
7 | export default function UsernameEditor({ username }: Props) {
8 | const [newUsername, setNewUsername] = useState(username);
9 | const user = useUser();
10 |
11 | const saveUsername = async () => {
12 | try {
13 | const resp = await fetch("/api/users", {
14 | method: "POST",
15 | body: JSON.stringify({
16 | userId: user?.id,
17 | username: newUsername,
18 | }),
19 | });
20 | const json = await resp.json();
21 | if (json.error) {
22 | alert(json.error);
23 | }
24 | } catch (error: any) {
25 | alert(error.message);
26 | }
27 | };
28 |
29 | return (
30 |
31 |
32 |
Enter URL and Title
33 |
setNewUsername(e.target.value)}
38 | />
39 |
43 | Save
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/Components/Setup/LinksPreviewComponent.tsx:
--------------------------------------------------------------------------------
1 | import { ClipboardIcon, XMarkIcon } from "@heroicons/react/24/solid";
2 | import { useState } from "react";
3 | import { Database } from "../../types/supabase";
4 | import Link from "next/link";
5 |
6 | type Props = {
7 | username: string;
8 | links: Database["public"]["Tables"]["Links"]["Row"][];
9 | };
10 | export default function LinksPreviewComponent({ links, username }: Props) {
11 | const [shouldShowSuccessToast, setShouldShowSuccessToast] =
12 | useState(false);
13 | const showSuccessToast = () => {
14 | setShouldShowSuccessToast(true);
15 | setTimeout(() => {
16 | setShouldShowSuccessToast(false);
17 | }, 3000);
18 | };
19 | return (
20 |
21 |
{
24 | event.preventDefault();
25 | await navigator.clipboard.writeText(
26 | `${process.env.NEXT_PUBLIC_BASE_URL}/${username}`
27 | );
28 | showSuccessToast();
29 | }}
30 | >
31 |
{`${process.env.NEXT_PUBLIC_BASE_URL}/${username}`}
32 |
33 | copy{" "}
34 |
35 |
setShouldShowSuccessToast(false)}
40 | >
41 |
Link copied!
42 |
43 |
44 |
45 |
49 |
50 | {links.map((link) => (
51 |
56 | {link.title}
57 |
58 | ))}
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/Components/Setup/LinksSetupComponent.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useUser } from "@supabase/auth-helpers-react";
3 | import { Database } from "../../types/supabase";
4 | import { XMarkIcon } from "@heroicons/react/24/solid";
5 | import { useRouter } from "next/router";
6 |
7 | type Props = {
8 | links: Database["public"]["Tables"]["Links"]["Row"][];
9 | };
10 | export default function LinksSetupComponent({ links }: Props) {
11 | const [url, setUrl] = useState("");
12 | const [title, setTitle] = useState("");
13 | const router = useRouter();
14 |
15 | const refreshPage = () => router.replace(router.asPath);
16 |
17 | const user = useUser();
18 |
19 | const addLink = async () => {
20 | try {
21 | if (!user) {
22 | alert("You must be logged in to add a link");
23 | return;
24 | }
25 |
26 | const resp = await fetch("/api/links", {
27 | method: "POST",
28 | body: JSON.stringify({
29 | url,
30 | title,
31 | userId: user.id,
32 | }),
33 | });
34 | const json = await resp.json();
35 | if (json.error) {
36 | alert(json.error);
37 | } else {
38 | resetState();
39 | void refreshPage();
40 | }
41 | } catch (error: any) {
42 | alert(error.message);
43 | }
44 | };
45 |
46 | const deleteLink = async (linkId: number) => {
47 | try {
48 | if (!user) {
49 | alert("You must be logged in to delete a link");
50 | return;
51 | }
52 |
53 | const resp = await fetch(
54 | `/api/links?userId=${user.id}&linkId=${linkId}`,
55 | {
56 | method: "DELETE",
57 | }
58 | );
59 | const json = await resp.json();
60 | if (json.error) {
61 | alert(json.error);
62 | } else {
63 | void refreshPage();
64 | resetState();
65 | }
66 | } catch (error: any) {
67 | alert(error.message);
68 | }
69 | };
70 |
71 | const resetState = () => {
72 | setUrl("");
73 | setTitle("");
74 | };
75 |
76 | return (
77 |
78 |
79 |
Enter URL and Title
80 |
setTitle(e.target.value)}
85 | />
86 |
setUrl(e.target.value)}
91 | />
92 |
96 | Add Link
97 |
98 |
99 | {links.map((link) => (
100 |
106 |
107 |
{link.title}
108 |
{link.url}
109 |
110 |
deleteLink(link.id)}>
111 |
112 |
113 |
114 | ))}
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/Components/common/BackgroundGradient.tsx:
--------------------------------------------------------------------------------
1 | export default function BackgroundGradient() {
2 | return
3 |
4 |
10 |
15 |
16 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | }
--------------------------------------------------------------------------------
/projects/LinkTreeClone/Components/common/Header.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
3 | import { useRouter } from "next/router";
4 | import { useState } from "react";
5 |
6 | export default function Header() {
7 | const [supabase] = useState(() => createBrowserSupabaseClient());
8 | const router = useRouter();
9 |
10 | const handleSignOut = async () => {
11 | const { error } = await supabase.auth.signOut();
12 | if (error) {
13 | alert(error.message);
14 | } else {
15 | await router.push("/");
16 | }
17 | };
18 |
19 | const navigateToAccountPage = async () => {
20 | await router.push("/account");
21 | };
22 |
23 | return (
24 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/README.md:
--------------------------------------------------------------------------------
1 | # LinkTree Clone
2 |
3 | This is a clone of the popular website Linktree, a link aggregator website that allows users to create a page with links
4 | to their social media profiles. [Here](https://www.beacons.ai/youraveragetechbro) is an example of one.
5 |
6 | [Here](https://youtu.be/rVRaV-qctm4) is a demo of what the app looks like.
7 |
8 |
9 | ## How To Run Locally
10 |
11 | To run the game locally, run `npm install` to install all the necessary dependencies and then run `npm run dev` to run web appp.
12 |
13 | This project uses [Supabase](https://supabase.com/) as the database. To set it up, create a `.env` file at the
14 | root directory and include the following env variables.
15 |
16 | ```
17 | NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
18 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_project_anon_public_key
19 | ```
20 |
21 | You can find the values for the two env variables in your Supabase project settings in the `API` tab.
22 |
23 | Once you add your Supabase keys, you need to create the necessary tables in Supabase.
24 |
25 | You find the database schema in the [types/supabase.ts](https://www.sweprojects.com/projects/preview/clh7qgfw30000vr1re1505imehttps://github.com/YourAverageTechBro/SWEProjects/blob/main/projects/LinkTreeClone/types/supabase.ts) file.
26 |
27 | ## Want Step-By-Step Instructions?
28 |
29 | If you want a step-by-step tutorial on how to build this project, you can find the tutorial on [here](https://www.sweprojects.com/projects/preview/clh7qgfw30000vr1re1505ime).
30 |
31 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-project",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@heroicons/react": "^2.0.17",
13 | "@supabase/auth-helpers-nextjs": "^0.6.0",
14 | "@supabase/auth-helpers-react": "^0.3.1",
15 | "@supabase/auth-ui-react": "^0.3.5",
16 | "@supabase/auth-ui-shared": "^0.1.3",
17 | "@supabase/supabase-js": "^2.21.0",
18 | "@types/node": "18.15.11",
19 | "@types/react": "18.0.35",
20 | "@types/react-dom": "18.0.11",
21 | "autoprefixer": "10.4.14",
22 | "eslint": "8.38.0",
23 | "eslint-config-next": "13.3.0",
24 | "next": "13.3.0",
25 | "postcss": "8.4.22",
26 | "react": "18.2.0",
27 | "react-dom": "18.2.0",
28 | "tailwindcss": "3.3.1",
29 | "typescript": "5.0.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/[username].tsx:
--------------------------------------------------------------------------------
1 | import { GetServerSideProps } from "next";
2 | import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
3 | import { Database } from "../types/supabase";
4 | import Link from "next/link";
5 |
6 | type Props = {
7 | links: Database["public"]["Tables"]["Links"]["Row"][];
8 | username: string;
9 | };
10 | export default function CreatorPage({ links, username }: Props) {
11 | return (
12 |
13 |
Links for {username}
14 |
15 | {links.map((link) => (
16 |
21 | {link.title}
22 |
23 | ))}
24 |
25 |
26 | );
27 | }
28 |
29 | export const getServerSideProps: GetServerSideProps = async (context) => {
30 | const supabase = createServerSupabaseClient(context);
31 | const { username } = context.params as { username: string };
32 | const {
33 | data: { session },
34 | } = await supabase.auth.getSession();
35 |
36 | if (!session || !username)
37 | return {
38 | redirect: {
39 | destination: "/",
40 | permanent: false,
41 | },
42 | };
43 | const { data: user, error: fetchUsernameError } = await supabase
44 | .from("Users")
45 | .select("*")
46 | .eq("username", username)
47 | .single();
48 |
49 | if (fetchUsernameError) {
50 | return {
51 | redirect: {
52 | destination: "/",
53 | permanent: false,
54 | },
55 | };
56 | }
57 |
58 | const { data: links, error: fetchLinksError } = await supabase
59 | .from("Links")
60 | .select("*")
61 | .eq("user_id", user.id);
62 |
63 | if (fetchLinksError) {
64 | return {
65 | redirect: {
66 | destination: "/",
67 | permanent: false,
68 | },
69 | };
70 | }
71 |
72 | return {
73 | props: {
74 | links,
75 | username,
76 | },
77 | };
78 | };
79 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import type { AppProps } from "next/app";
3 | import { useState } from "react";
4 | import { SessionContextProvider } from "@supabase/auth-helpers-react";
5 | import {
6 | createBrowserSupabaseClient,
7 | Session,
8 | } from "@supabase/auth-helpers-nextjs";
9 | import { Database } from "../types/supabase";
10 |
11 | export default function App({
12 | Component,
13 | pageProps,
14 | }: AppProps<{ initialSession: Session }>) {
15 | const [supabaseClient] = useState(() =>
16 | createBrowserSupabaseClient()
17 | );
18 | return (
19 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/account.tsx:
--------------------------------------------------------------------------------
1 | import Header from "../Components/common/Header";
2 | import UsernameEditor from "../Components/Account/UsernameEditor";
3 | import { GetServerSideProps } from "next";
4 | import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
5 |
6 | type Props = {
7 | username: string;
8 | };
9 | export default function Account({ username }: Props) {
10 | return (
11 |
12 |
13 |
14 |
15 | );
16 | }
17 | export const getServerSideProps: GetServerSideProps = async (context) => {
18 | const supabase = createServerSupabaseClient(context);
19 | const {
20 | data: { session },
21 | } = await supabase.auth.getSession();
22 |
23 | const userId = session?.user.id;
24 | if (!userId) {
25 | return {
26 | redirect: {
27 | destination: "/",
28 | permanent: false,
29 | },
30 | };
31 | }
32 | const { data: user, error: fetchUsernameError } = await supabase
33 | .from("Users")
34 | .select("*")
35 | .eq("id", userId)
36 | .single();
37 |
38 | if (fetchUsernameError) {
39 | return {
40 | redirect: {
41 | destination: "/",
42 | permanent: false,
43 | },
44 | };
45 | }
46 |
47 | return {
48 | props: {
49 | username: user.username,
50 | },
51 | };
52 | };
53 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/api/links.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { Database } from "../../types/supabase";
3 | import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
4 |
5 | type Data = {
6 | error?: string;
7 | links?: Database["public"]["Tables"]["Links"]["Row"][];
8 | };
9 | export default async function handler(
10 | req: NextApiRequest,
11 | res: NextApiResponse
12 | ) {
13 | try {
14 | if (req.method === "POST") {
15 | await handlePost(req, res);
16 | } else if (req.method === "GET") {
17 | await handleGet(req, res);
18 | } else if (req.method === "DELETE") {
19 | await handleDelete(req, res);
20 | }
21 | } catch (error: any) {
22 | res.status(500).json({ error: error.message });
23 | }
24 | }
25 |
26 | const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
27 | const supabaseServerClient = createServerSupabaseClient({
28 | req,
29 | res,
30 | });
31 | const body = JSON.parse(req.body);
32 | const { url, title, userId } = body as {
33 | url: string;
34 | title: string;
35 | userId: string;
36 | };
37 |
38 | if (!url || !title || !userId) {
39 | res.status(400).json({ error: "Missing url, title, or userId" });
40 | return;
41 | }
42 |
43 | const { error: insertError } = await supabaseServerClient
44 | .from("Links")
45 | .insert({
46 | url,
47 | title,
48 | user_id: userId,
49 | });
50 |
51 | if (insertError) {
52 | res.status(500).json({ error: insertError.message });
53 | } else {
54 | res.status(200).json({});
55 | }
56 | };
57 |
58 | const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
59 | const supabaseServerClient = createServerSupabaseClient({
60 | req,
61 | res,
62 | });
63 | const { userId } = req.query;
64 |
65 | if (!userId) {
66 | res.status(400).json({ error: "Missing userId" });
67 | return;
68 | }
69 |
70 | const { data, error } = await supabaseServerClient
71 | .from("Links")
72 | .select("*")
73 | .eq("user_id", userId);
74 | if (error) {
75 | res.status(500).json({ error: error.message });
76 | } else {
77 | res.status(200).json({ links: data });
78 | }
79 | };
80 |
81 | const handleDelete = async (
82 | req: NextApiRequest,
83 | res: NextApiResponse
84 | ) => {
85 | const supabaseServerClient = createServerSupabaseClient({
86 | req,
87 | res,
88 | });
89 | const { linkId, userId } = req.query;
90 |
91 | if (!userId || !linkId) {
92 | res.status(400).json({ error: "Invalid parameters" });
93 | return;
94 | }
95 |
96 | const { error: deleteError } = await supabaseServerClient
97 | .from("Links")
98 | .delete()
99 | .eq("user_id", userId)
100 | .eq("id", linkId);
101 | if (deleteError) {
102 | res.status(500).json({ error: deleteError.message });
103 | } else {
104 | res.status(200).json({});
105 | }
106 | };
107 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/api/usernames.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { Database } from "../../types/supabase";
3 | import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
4 |
5 | type Data = {
6 | error?: string;
7 | usernames?: Database["public"]["Tables"]["Users"]["Row"]["username"][];
8 | };
9 | export default async function handler(
10 | req: NextApiRequest,
11 | res: NextApiResponse
12 | ) {
13 | try {
14 | if (req.method === "GET") {
15 | await handleGet(req, res);
16 | }
17 | } catch (error: any) {
18 | res.status(500).json({ error: error.message });
19 | }
20 | }
21 | const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
22 | const supabaseServerClient = createServerSupabaseClient({
23 | req,
24 | res,
25 | });
26 |
27 | const { data, error } = await supabaseServerClient
28 | .from("Users")
29 | .select("username");
30 | if (error) {
31 | res.status(500).json({ error: error.message });
32 | } else {
33 | res.status(200).json({ usernames: data.map((data) => data.username) });
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/api/users.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { Database } from "../../types/supabase";
3 | import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
4 |
5 | type Data = {
6 | error?: string;
7 | user?: Database["public"]["Tables"]["Users"]["Row"];
8 | };
9 | export default async function handler(
10 | req: NextApiRequest,
11 | res: NextApiResponse
12 | ) {
13 | try {
14 | if (req.method === "POST") {
15 | await handlePost(req, res);
16 | }
17 | } catch (error: any) {
18 | res.status(500).json({ error: error.message });
19 | }
20 | }
21 | const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
22 | const supabaseServerClient = createServerSupabaseClient({
23 | req,
24 | res,
25 | });
26 | const body = JSON.parse(req.body);
27 | const { username, userId } = body as {
28 | username: string;
29 | userId: string;
30 | };
31 |
32 | if (!username || !userId) {
33 | res.status(400).json({ error: "Missing username or userId" });
34 | return;
35 | }
36 |
37 | const { error: updateError } = await supabaseServerClient
38 | .from("Users")
39 | .update({
40 | username,
41 | })
42 | .eq("id", userId);
43 |
44 | if (updateError) {
45 | res.status(500).json({ error: updateError.message });
46 | } else {
47 | res.status(200).json({});
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/auth.tsx:
--------------------------------------------------------------------------------
1 | import { Auth } from "@supabase/auth-ui-react";
2 | import { ThemeSupa } from "@supabase/auth-ui-shared";
3 | import { useEffect, useState } from "react";
4 | import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
5 | import { useRouter } from "next/router";
6 |
7 | export default function AuthPage() {
8 | const [supabaseClient] = useState(() => createBrowserSupabaseClient());
9 | const router = useRouter();
10 |
11 | useEffect(() => {
12 | const {
13 | data: { subscription },
14 | } = supabaseClient.auth.onAuthStateChange(async (event, session) => {
15 | const userEmail = session?.user.email;
16 | const userId = session?.user.id;
17 | if (event === "SIGNED_IN" && userId && userEmail) {
18 | await router.push(`/setup`);
19 | }
20 | });
21 | return () => {
22 | subscription?.unsubscribe();
23 | };
24 | }, [router, supabaseClient.auth]);
25 |
26 | return (
27 |
28 |
29 |
30 |
Welcome to LinkTree Clone.
31 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import BackgroundGradient from "../Components/common/BackgroundGradient";
2 | import Link from "next/link";
3 |
4 | export default function Home() {
5 | return (
6 |
7 |
8 |
9 |
10 | Create your link in bio
11 |
12 |
16 | Get Started
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/pages/setup.tsx:
--------------------------------------------------------------------------------
1 | import { Database } from "../types/supabase";
2 | import LinksPreviewComponent from "../Components/Setup/LinksPreviewComponent";
3 | import LinksSetupComponent from "../Components/Setup/LinksSetupComponent";
4 | import Header from "../Components/common/Header";
5 | import { GetServerSideProps } from "next";
6 | import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
7 |
8 | type Props = {
9 | links: Database["public"]["Tables"]["Links"]["Row"][];
10 | username: string;
11 | };
12 | export default function Setup({ links, username }: Props) {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | export const getServerSideProps: GetServerSideProps = async (context) => {
24 | const supabase = createServerSupabaseClient(context);
25 | const {
26 | data: { session },
27 | } = await supabase.auth.getSession();
28 |
29 | const userId = session?.user.id;
30 | if (!userId) {
31 | return {
32 | redirect: {
33 | destination: "/",
34 | permanent: false,
35 | },
36 | };
37 | }
38 |
39 | const { data: user, error: fetchUsernameError } = await supabase
40 | .from("Users")
41 | .select("*")
42 | .eq("id", userId)
43 | .single();
44 |
45 | if (fetchUsernameError) {
46 | return {
47 | redirect: {
48 | destination: "/",
49 | permanent: false,
50 | },
51 | };
52 | }
53 |
54 | const { data: links, error: fetchLinksError } = await supabase
55 | .from("Links")
56 | .select("*")
57 | .eq("user_id", userId);
58 |
59 | if (fetchLinksError) {
60 | return {
61 | redirect: {
62 | destination: "/",
63 | permanent: false,
64 | },
65 | };
66 | }
67 |
68 | return {
69 | props: {
70 | links,
71 | username: user.username,
72 | },
73 | };
74 | };
75 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/projects/LinkTreeClone/public/favicon.ico
--------------------------------------------------------------------------------
/projects/LinkTreeClone/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: light) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './pages/**/*.{js,ts,jsx,tsx}',
5 | './components/**/*.{js,ts,jsx,tsx}',
6 | './app/**/*.{js,ts,jsx,tsx}',
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12 | 'gradient-conic':
13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | }
19 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/*": ["./*"]
19 | }
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/projects/LinkTreeClone/types/supabase.ts:
--------------------------------------------------------------------------------
1 | export type Json =
2 | | string
3 | | number
4 | | boolean
5 | | null
6 | | { [key: string]: Json }
7 | | Json[]
8 |
9 | export interface Database {
10 | public: {
11 | Tables: {
12 | Links: {
13 | Row: {
14 | created_at: string | null
15 | id: number
16 | title: string
17 | url: string
18 | user_id: string
19 | }
20 | Insert: {
21 | created_at?: string | null
22 | id?: number
23 | title: string
24 | url: string
25 | user_id: string
26 | }
27 | Update: {
28 | created_at?: string | null
29 | id?: number
30 | title?: string
31 | url?: string
32 | user_id?: string
33 | }
34 | }
35 | Users: {
36 | Row: {
37 | created_at: string | null
38 | id: string
39 | username: string
40 | }
41 | Insert: {
42 | created_at?: string | null
43 | id: string
44 | username: string
45 | }
46 | Update: {
47 | created_at?: string | null
48 | id?: string
49 | username?: string
50 | }
51 | }
52 | }
53 | Views: {
54 | [_ in never]: never
55 | }
56 | Functions: {
57 | [_ in never]: never
58 | }
59 | Enums: {
60 | [_ in never]: never
61 | }
62 | CompositeTypes: {
63 | [_ in never]: never
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/public/favicon.ico
--------------------------------------------------------------------------------
/src/components/Common/BackgroundGradient.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function BackgroundGradient() {
4 | return (
5 |
6 |
12 |
17 |
18 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
--------------------------------------------------------------------------------
/src/components/Common/Header.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | SignedIn,
3 | SignedOut,
4 | SignInButton,
5 | UserButton,
6 | useUser,
7 | } from "@clerk/nextjs";
8 | import { Button } from "src/components/LandingPage/Button";
9 | import Link from "next/link";
10 | import { Logo } from "~/components/LandingPage/Logo";
11 | import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react";
12 |
13 | export default function Header() {
14 | const { user } = useUser();
15 | const postHog = usePostHog();
16 | const isNewProjectCreationFlowEnabled = useFeatureFlagEnabled(
17 | "new-project-creation-flow"
18 | );
19 |
20 | return (
21 |
29 |
30 | {
33 | postHog?.identify(user?.id, {
34 | name: user?.fullName,
35 | email: user?.primaryEmailAddress?.emailAddress,
36 | });
37 | postHog?.capture("Click on Logo", {
38 | distinct_id: user?.id,
39 | time: new Date(),
40 | });
41 | }}
42 | />
43 |
44 |
45 |
46 | {isNewProjectCreationFlowEnabled && (
47 |
53 | {" "}
54 | My Project Tutorials
55 |
56 | )}
57 | {
62 | postHog?.identify(user?.id, {
63 | name: user?.fullName,
64 | email: user?.primaryEmailAddress?.emailAddress,
65 | });
66 | postHog?.capture("Click on my projects", {
67 | distinct_id: user?.id,
68 | time: new Date(),
69 | });
70 | }}
71 | >
72 | {" "}
73 | Purchased projects{" "}
74 |
75 | {
80 | postHog?.identify(user?.id, {
81 | name: user?.fullName,
82 | email: user?.primaryEmailAddress?.emailAddress,
83 | });
84 | postHog?.capture("Click on view all projects", {
85 | distinct_id: user?.id,
86 | time: new Date(),
87 | });
88 | }}
89 | >
90 | View all projects
91 |
92 |
93 |
94 |
95 | {/* Signed-out users get sign in button */}
96 |
97 | {/*eslint-disable-next-line @typescript-eslint/ban-ts-comment*/}
98 | {/*@ts-ignore*/}
99 |
100 | Sign in
101 |
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/src/components/Common/LoadingSpinner.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | styleOverride?: string;
3 | spinnerColor?: string;
4 | };
5 | export default function LoadingSpinner({
6 | styleOverride,
7 | spinnerColor = "fill-blue-600 text-gray-200 dark:text-gray-600",
8 | }: Props) {
9 | return (
10 |
14 |
21 |
25 |
29 |
30 |
Loading...
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Common/MdEditor/MdEditor.tsx:
--------------------------------------------------------------------------------
1 | import { type Dispatch, type SetStateAction } from "react";
2 | import { onImagePasted } from "./utils";
3 | import "@uiw/react-md-editor/markdown-editor.css";
4 | import "@uiw/react-markdown-preview/markdown.css";
5 | import { createClient } from "@supabase/supabase-js";
6 | import dynamic from "next/dynamic";
7 |
8 | const MDEditor = dynamic(
9 | () => import("@uiw/react-md-editor").then((mod) => mod.default),
10 | { ssr: false }
11 | );
12 |
13 | type Props = {
14 | value: string | undefined;
15 | onChange: Dispatch>;
16 | index: number;
17 | hideToolbar: boolean;
18 | preview: "edit" | "preview" | "live";
19 | height: string;
20 | };
21 | const MdEditor = ({
22 | value,
23 | onChange,
24 | index,
25 | preview,
26 | hideToolbar,
27 | height,
28 | }: Props) => {
29 | const supabase = createClient(
30 | process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
31 | process.env.NEXT_PUBLIC_SUPABASE_SERVICE_SECRET_KEY ?? ""
32 | );
33 |
34 | // eslint-disable @typescript-eslint/ban-ts-comment
35 | return (
36 | {
41 | onChange(value);
42 | }}
43 | /* eslint-disable-next-line @typescript-eslint/no-misused-promises */
44 | onPaste={async (event) => {
45 | await onImagePasted(event.clipboardData, onChange, supabase, index);
46 | }}
47 | /* eslint-disable-next-line @typescript-eslint/no-misused-promises */
48 | onDrop={async (event) => {
49 | await onImagePasted(event.dataTransfer, onChange, supabase, index);
50 | }}
51 | /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
52 | /* @ts-ignore */
53 | height={height}
54 | />
55 | );
56 | };
57 |
58 | export default MdEditor;
59 |
--------------------------------------------------------------------------------
/src/components/Common/MdEditor/utils.tsx:
--------------------------------------------------------------------------------
1 | import type { SetStateAction } from "react";
2 | import { type SupabaseClient } from "@supabase/supabase-js";
3 |
4 | export const insertTextToArea = (
5 | insertString: string,
6 | textAreaIndex: number
7 | ) => {
8 | const textareas = document.querySelectorAll(
9 | "textarea.w-md-editor-text-input"
10 | );
11 | if (textareas.length === 0) {
12 | return null;
13 | }
14 | const textarea = textareas[textAreaIndex] as HTMLTextAreaElement;
15 | if (!textarea) return null;
16 |
17 | let sentence = textarea.value;
18 | const len = sentence.length;
19 | const pos = textarea.selectionStart;
20 | const end = textarea.selectionEnd;
21 |
22 | const front = sentence.slice(0, pos);
23 | const back = sentence.slice(pos, len);
24 |
25 | sentence = front + insertString + back;
26 |
27 | textarea.value = sentence;
28 | textarea.selectionEnd = end + insertString.length;
29 |
30 | return sentence;
31 | };
32 |
33 | export const onImagePasted = async (
34 | dataTransfer: DataTransfer,
35 | setMarkdown: (value: SetStateAction) => void,
36 | supabase: SupabaseClient,
37 | textAreaIndex: number
38 | ) => {
39 | const files: File[] = [];
40 | for (let index = 0; index < dataTransfer.items.length; index += 1) {
41 | const file = dataTransfer.files.item(index);
42 |
43 | if (file) {
44 | files.push(file);
45 | }
46 | }
47 |
48 | await Promise.all(
49 | files.map(async (file) => {
50 | const url = await fileUpload(file, supabase);
51 | const insertedMarkdown = insertTextToArea(``, textAreaIndex);
52 | if (!insertedMarkdown) {
53 | return;
54 | }
55 | setMarkdown(insertedMarkdown);
56 | })
57 | );
58 | };
59 |
60 | export const fileUpload = async (file: File, supabase: SupabaseClient) => {
61 | const { data, error } = await supabase.storage
62 | .from("public/images")
63 | .upload(`public/images${file.name}`, file, {
64 | cacheControl: "3600",
65 | upsert: true,
66 | });
67 |
68 | if (error) throw error;
69 | const resp = supabase.storage.from("public/images").getPublicUrl(data.path);
70 | return resp.data.publicUrl;
71 | };
72 |
--------------------------------------------------------------------------------
/src/components/Common/PhotoUploadComponent.tsx:
--------------------------------------------------------------------------------
1 | import ImageUploading, { type ImageListType } from "react-images-uploading";
2 | import { XCircleIcon } from "@heroicons/react/24/solid";
3 |
4 | type Props = {
5 | onSaveCallback: () => Promise;
6 | images: ImageListType;
7 | setImages: (images: ImageListType) => void;
8 | caption: string;
9 | setCaption: (caption: string) => void;
10 | };
11 | export default function PhotoUploadComponent({
12 | onSaveCallback,
13 | images,
14 | setImages,
15 | caption,
16 | setCaption,
17 | }: Props) {
18 | const onContentImageChange = (imageList: ImageListType) => {
19 | setImages(imageList);
20 | };
21 |
22 | const saveImages = async () => {
23 | await onSaveCallback();
24 | };
25 |
26 | return (
27 |
28 |
34 | {({
35 | imageList,
36 | onImageUpload,
37 | onImageRemove,
38 | isDragging,
39 | dragProps,
40 | }) => (
41 |
42 | {images.length === 0 && (
43 |
49 | Click or Drop An Image Here
50 |
51 | )}
52 |
53 | {imageList.map((image, index) => (
54 |
55 |
61 |
62 | onImageRemove(index)}
65 | />
66 |
67 |
setCaption(e.target.value)}
73 | />
74 |
75 | ))}
76 |
77 | )}
78 |
79 |
80 | {
83 | void (async () => {
84 | await saveImages();
85 | })();
86 | }}
87 | >
88 | Save
89 |
90 |
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/Common/Selectors.tsx:
--------------------------------------------------------------------------------
1 | import { type Dispatch, Fragment, type SetStateAction, useEffect } from "react";
2 | import { Listbox, Transition } from "@headlessui/react";
3 | import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
4 |
5 | function classNames(...classes: string[]) {
6 | return classes.filter(Boolean).join(" ");
7 | }
8 |
9 | type Props = {
10 | values: string[];
11 | selected: string;
12 | setSelected: Dispatch>;
13 | label: string;
14 | };
15 |
16 | export default function Selectors({
17 | selected,
18 | setSelected,
19 | values,
20 | label,
21 | }: Props) {
22 | useEffect(() => {
23 | if (!selected && values[0]) {
24 | setSelected(values[0]);
25 | }
26 | }, [selected, setSelected, values]);
27 | return (
28 |
29 | {({ open }) => (
30 | <>
31 |
32 | {label}
33 |
34 |
35 |
36 | {selected}
37 |
38 |
42 |
43 |
44 |
45 |
52 |
53 | {values.map((value, index) => (
54 |
57 | classNames(
58 | active ? "bg-indigo-600 text-white" : "text-gray-900",
59 | "relative cursor-default select-none py-2 pl-3 pr-9"
60 | )
61 | }
62 | value={value}
63 | >
64 | {({ selected, active }) => (
65 | <>
66 |
72 | {value}
73 |
74 |
75 | {selected ? (
76 |
82 |
83 |
84 | ) : null}
85 | >
86 | )}
87 |
88 | ))}
89 |
90 |
91 |
92 | >
93 | )}
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/DraftProjects/DraftInstructionalTextComponent.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { api } from "~/utils/api";
3 | import { toast } from "react-hot-toast";
4 | import { debounce } from "throttle-debounce";
5 | import "@uiw/react-md-editor/markdown-editor.css";
6 | import "@uiw/react-markdown-preview/markdown.css";
7 | import MdEditor from "~/components/Common/MdEditor/MdEditor";
8 |
9 | type Props = {
10 | initialValue?: string;
11 | instructionId: string;
12 | index: number;
13 | readOnly: boolean;
14 | isAuthor: boolean;
15 | };
16 | export default function DraftInstructionalTextComponent({
17 | initialValue,
18 | instructionId,
19 | index,
20 | readOnly,
21 | isAuthor,
22 | }: Props) {
23 | const [explanation, setExplanation] = useState(
24 | initialValue
25 | );
26 | const [isInitialized, setIsInitialized] = useState(false);
27 |
28 | const debouncedSave = useCallback(
29 | debounce(1000, (instructionId: string, explanation: string) => {
30 | mutate({ instructionId, explanation });
31 | }),
32 | []
33 | );
34 |
35 | useEffect(() => {
36 | try {
37 | if (explanation && isAuthor) {
38 | debouncedSave(instructionId, explanation);
39 | }
40 | } catch (error: any) {}
41 | }, [explanation, debouncedSave]);
42 |
43 | const { mutate, isLoading: isSaving } = api.instructions.update.useMutation({
44 | onSuccess: (project) => {
45 | if (!isInitialized) {
46 | setIsInitialized(true);
47 | } else {
48 | // Show successfully saved state
49 | toast.success("Saved!");
50 | }
51 | },
52 | onError: (e) => {
53 | const errorMessage = e.data?.zodError?.fieldErrors.content;
54 | if (errorMessage && errorMessage[0]) {
55 | toast.error(errorMessage[0]);
56 | } else {
57 | toast.error("Failed to save! Please try again later.");
58 | }
59 | },
60 | });
61 |
62 | return (
63 |
67 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/DraftProjects/Editor.tsx:
--------------------------------------------------------------------------------
1 | // import logo from './logo.svg'
2 | import "@blocknote/core/style.css";
3 | import { BlockNoteView, useBlockNote } from "@blocknote/react";
4 | import { type Block, type BlockNoteEditor } from "@blocknote/core";
5 | import { type Dispatch, type SetStateAction } from "react";
6 |
7 | type Props = {
8 | initialValue: Block[] | undefined;
9 | onChange: Dispatch>;
10 | };
11 | function Editor({ initialValue, onChange }: Props) {
12 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
13 | const editor: BlockNoteEditor | null = useBlockNote({
14 | onEditorContentChange: (editor: BlockNoteEditor) => {
15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
16 | onChange(editor.topLevelBlocks);
17 | },
18 | initialContent: initialValue ?? undefined,
19 | });
20 |
21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
22 | return ;
23 | }
24 |
25 | export default Editor;
26 |
--------------------------------------------------------------------------------
/src/components/DraftProjects/FocusedQuestion.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowSmallLeftIcon } from "@heroicons/react/24/solid";
2 | import { type Dispatch, type SetStateAction } from "react";
3 | import { type Questions } from "@prisma/client";
4 | import CommentBox from "~/components/QuestionsAndAnswers/CommentBox";
5 | import CommentsList from "~/components/QuestionsAndAnswers/CommentsList";
6 |
7 | type Props = {
8 | question: Questions;
9 | setFocusedQuestion: Dispatch>;
10 | };
11 | export default function FocusedQuestionComponent({
12 | question,
13 | setFocusedQuestion,
14 | }: Props) {
15 | return (
16 |
17 |
setFocusedQuestion(undefined)}>
18 |
19 |
20 |
{question.title}
21 |
{question.question}
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/DraftProjects/InstructionLeftSidebar.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import DraftInstructionalTextComponent from "~/components/DraftProjects/DraftInstructionalTextComponent";
3 | import QuestionsPlaceholder from "~/components/Images/Questions";
4 |
5 | enum SideBarContent {
6 | INSTRUCTIONS = "Instructions",
7 | QUESTIONS = "Questions",
8 | }
9 |
10 | type Props = {
11 | explanation: string;
12 | instructionId: string;
13 | index: number;
14 | isEditing: boolean;
15 | isAuthor: boolean;
16 | isQAFeatureEnabled: boolean;
17 | };
18 | export default function InstructionLeftSidebar({
19 | explanation,
20 | instructionId,
21 | index,
22 | isEditing,
23 | isAuthor,
24 | isQAFeatureEnabled,
25 | }: Props) {
26 | const [focusedSideBarContent, setFocusedSideBarContent] = useState(
27 | SideBarContent.INSTRUCTIONS
28 | );
29 | return (
30 |
31 |
32 | {Object.values(SideBarContent).map((sideBarContent, index) => (
33 |
39 | setFocusedSideBarContent(sideBarContent as SideBarContent)
40 | }
41 | >
42 | {sideBarContent}
43 |
44 | ))}
45 |
46 |
47 | {focusedSideBarContent === SideBarContent.INSTRUCTIONS && (
48 |
55 | )}
56 | {focusedSideBarContent === SideBarContent.QUESTIONS &&
57 | !isQAFeatureEnabled && (
58 |
63 | {" "}
64 |
{" "}
65 |
66 | {" "}
67 | We are launching the questions feature soon!{" "}
68 |
69 |
70 | Hold onto your questions for now and we will let you know when
71 | you can add them here!
72 |
73 |
74 | )}
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/LandingPage/AuthLayout.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import backgroundImage from "./images/background-auth.jpg";
4 | import React from "react";
5 |
6 | type Props = {
7 | children: React.ReactNode;
8 | };
9 | export function AuthLayout({ children }: Props) {
10 | return (
11 | <>
12 |
13 |
14 |
15 | {children}
16 |
17 |
18 |
19 |
25 |
26 |
27 | >
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Button.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import clsx from "clsx";
3 |
4 | const baseStyles = {
5 | solid:
6 | "group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2",
7 | outline:
8 | "group inline-flex ring-1 items-center justify-center rounded-full py-2 px-4 text-sm focus:outline-none",
9 | };
10 |
11 | const variantStyles = {
12 | solid: {
13 | slate:
14 | "bg-slate-900 text-white hover:bg-slate-700 hover:text-slate-100 active:bg-slate-800 active:text-slate-300 focus-visible:outline-slate-900",
15 | blue: "bg-blue-600 text-white hover:text-slate-100 hover:bg-blue-500 active:bg-blue-800 active:text-blue-100 focus-visible:outline-blue-600",
16 | white:
17 | "bg-white text-slate-900 hover:bg-blue-50 active:bg-blue-200 active:text-slate-600 focus-visible:outline-white",
18 | },
19 | outline: {
20 | slate:
21 | "ring-slate-200 text-slate-700 hover:text-slate-900 hover:ring-slate-300 active:bg-slate-100 active:text-slate-600 focus-visible:outline-blue-600 focus-visible:ring-slate-300",
22 | white:
23 | "ring-slate-700 text-white hover:ring-slate-500 active:ring-slate-700 active:text-slate-400 focus-visible:outline-white",
24 | },
25 | };
26 |
27 | type Props = {
28 | variant?: "solid" | "outline";
29 | color?: "slate" | "blue" | "white";
30 | className?: string;
31 | href?: string;
32 | };
33 |
34 | export function Button({
35 | variant = "solid",
36 | color = "slate",
37 | className,
38 | href,
39 | ...props
40 | }: Props) {
41 | className = clsx(
42 | baseStyles[variant],
43 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
44 | // @ts-ignore
45 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
46 | variantStyles[variant][color],
47 | className
48 | );
49 |
50 | return href ? (
51 |
52 | ) : (
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/LandingPage/CallToAction.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import { Button } from "src/components/LandingPage/Button";
4 | import { Container } from "~/components/LandingPage/Container";
5 | import backgroundImage from "./images/background-call-to-action.jpg";
6 | import { SignUpButton, useUser } from "@clerk/nextjs";
7 |
8 | export function CallToAction() {
9 | const { isSignedIn } = useUser();
10 |
11 | return (
12 |
16 |
24 |
25 |
26 |
27 | Get started today
28 |
29 |
30 | Ready to start building?
31 |
32 | {isSignedIn ? (
33 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
34 | // @ts-ignore
35 |
36 |
37 | Get started today
38 |
39 |
40 | ) : (
41 |
42 | {/*eslint-disable-next-line @typescript-eslint/ban-ts-comment*/}
43 | {/*@ts-ignore*/}
44 |
45 |
46 | Get started today
47 |
48 |
49 |
50 | )}
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Container.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4 | // @ts-ignore
5 | export function Container({ className, ...props }) {
6 | return (
7 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Faqs.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import { Container } from "src/components/LandingPage/Container";
4 | import backgroundImage from "./images/background-faqs.jpg";
5 |
6 | const faqs = [
7 | {
8 | question: "How long do I have access to a project once I purchase it?",
9 | answer: (
10 |
11 | {`You get lifetime access to the project. That means whenever we add a
12 | new tech stack variation to the project, you get lifetime access to
13 | that, too.`}
14 |
15 | ),
16 | },
17 | {
18 | question: "Do you offer any discounts?",
19 | answer: (
20 |
21 | {`Currently no, but we are looking into some ways to offer students
22 | discounts. If you sign up for an account, you'll be the first to know
23 | about any discounts.`}
24 |
25 | ),
26 | },
27 | {
28 | question: "Can I request a type of project to be made?",
29 | answer: (
30 |
31 | Yep, you definitely can. You can do so
32 |
36 | {" "}
37 | here{" "}
38 |
39 |
40 | ),
41 | },
42 | ];
43 |
44 | export function Faqs() {
45 | return (
46 |
51 |
59 |
60 |
61 |
65 | Frequently asked questions
66 |
67 |
68 | If you can’t find what you’re looking for, email our support team
69 | and if you’re lucky someone will get back to you.
70 |
71 |
72 |
73 | {faqs.map((column, columnIndex) => (
74 |
75 |
76 | {column.question}
77 |
78 | {column.answer}
79 |
80 | ))}
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Fields.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | const formClasses =
4 | "block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm";
5 |
6 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7 | // @ts-ignore
8 | function Label({ id, children }) {
9 | return (
10 |
15 | {children}
16 |
17 | );
18 | }
19 |
20 | export function TextField({
21 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
22 | // @ts-ignore
23 | id,
24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
25 | // @ts-ignore
26 | label,
27 | type = "text",
28 | className = "",
29 | ...props
30 | }) {
31 | return (
32 |
33 | {/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
34 | {label && {label} }
35 | {/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
36 |
37 |
38 | );
39 | }
40 |
41 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
42 | // @ts-ignore
43 | export function SelectField({ id, label, className = "", ...props }) {
44 | return (
45 |
46 | {/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
47 | {label && {label} }
48 | {/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { Container } from "src/components/LandingPage/Container";
4 | import { Logo } from "~/components/LandingPage/Logo";
5 | import { NavLink } from "~/components/LandingPage/NavLink";
6 | import { EnvelopeIcon, MapIcon } from "@heroicons/react/24/solid";
7 |
8 | export function Footer() {
9 | return (
10 |
11 | {/*eslint-disable-next-line @typescript-eslint/ban-ts-comment*/}
12 | {/*@ts-ignore*/}
13 |
14 |
15 |
16 |
17 |
18 | Features
19 | Testimonials
20 | FAQ
21 |
22 |
23 |
24 |
25 |
26 |
31 | Privacy Policy
32 |
33 |
38 | Terms of Service
39 |
40 |
45 |
46 |
47 |
52 |
53 |
54 |
55 |
56 | Copyright © {new Date().getFullYear()} SWEProjects. All rights
57 | reserved.
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Hero.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from "src/components/LandingPage/Container";
2 | import Link from "next/link";
3 |
4 | export function Hero() {
5 | return (
6 |
7 |
8 |
9 |
15 |
16 |
17 | Go Beyond Localhost
18 | {" "}
19 |
20 |
21 | Learn how to build high quality software projects that actually impress
22 | recruiters and employers.
23 |
24 |
25 | {/*eslint-disable-next-line @typescript-eslint/ban-ts-comment*/}
26 | {/*@ts-ignore*/}
27 |
31 | View all projects
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/LandingPage/NavLink.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4 | // @ts-ignore
5 | export function NavLink({ href, children }) {
6 | return (
7 |
12 | {children}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/LandingPage/images/avatars/avatar-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/avatars/avatar-1.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/avatars/avatar-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/avatars/avatar-2.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/avatars/avatar-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/avatars/avatar-3.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/avatars/avatar-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/avatars/avatar-4.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/avatars/avatar-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/avatars/avatar-5.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/avatars/avatar-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/avatars/avatar-6.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/background-auth.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/background-auth.jpg
--------------------------------------------------------------------------------
/src/components/LandingPage/images/background-call-to-action.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/background-call-to-action.jpg
--------------------------------------------------------------------------------
/src/components/LandingPage/images/background-faqs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/background-faqs.jpg
--------------------------------------------------------------------------------
/src/components/LandingPage/images/background-features.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/background-features.jpg
--------------------------------------------------------------------------------
/src/components/LandingPage/images/logos/laravel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
8 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/LandingPage/images/logos/mirage.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/LandingPage/images/logos/statickit.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/LandingPage/images/logos/transistor.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/LandingPage/images/logos/tuple.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/canny-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/canny-screenshot.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/contacts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/contacts.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/expenses.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/expenses.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/instructions-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/instructions-screenshot.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/inventory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/inventory.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/payroll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/payroll.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/profit-loss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/profit-loss.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/reporting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/reporting.png
--------------------------------------------------------------------------------
/src/components/LandingPage/images/screenshots/vat-returns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YourAverageTechBro/SWEProjects/4e820f4a98ecfa6fc8c2e7c6fa397ebcc796e209/src/components/LandingPage/images/screenshots/vat-returns.png
--------------------------------------------------------------------------------
/src/components/ProjectsV2/CodeDiffSection.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DiffEditor,
3 | type Monaco,
4 | type MonacoDiffEditor,
5 | } from "@monaco-editor/react";
6 | import { useCallback } from "react";
7 |
8 | type Props = {
9 | originalCode: string;
10 | modifiedCode: string;
11 | isAuthor: boolean;
12 | };
13 | export default function CodeDiffSection({
14 | originalCode,
15 | modifiedCode,
16 | isAuthor,
17 | }: Props) {
18 | const handleDiffEditorDidMount = useCallback(
19 | (editor: MonacoDiffEditor, monaco: Monaco) => {
20 | monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
21 | jsx: monaco.languages.typescript.JsxEmit.Preserve,
22 | target: monaco.languages.typescript.ScriptTarget.ES2020,
23 | esModuleInterop: true,
24 | });
25 | },
26 | []
27 | );
28 |
29 | return (
30 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/ProjectsV2/CompletedTutorial.tsx:
--------------------------------------------------------------------------------
1 | export default function CompletedTutorial() {
2 | return
;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/ProjectsV2/InstructionsToolbar.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CodeBracketSquareIcon,
3 | DocumentMagnifyingGlassIcon,
4 | } from "@heroicons/react/24/solid";
5 | import { api } from "~/utils/api";
6 | import { toast } from "react-hot-toast";
7 | import { type Instructions } from "@prisma/client";
8 | import { useRouter } from "next/router";
9 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
10 |
11 | type Props = {
12 | viewDiff: boolean;
13 | setViewDiff: (viewDiff: boolean) => void;
14 | currentInstruction: Instructions;
15 | };
16 | export default function InstructionsToolbar({
17 | currentInstruction,
18 | viewDiff,
19 | setViewDiff,
20 | }: Props) {
21 | const router = useRouter();
22 |
23 | const { mutate: updateInstruction, isLoading } =
24 | api.instructions.update.useMutation({
25 | onSuccess: () => {
26 | // Show successfully saved state
27 | toast.success("Saved!");
28 | void router.replace(router.asPath);
29 | },
30 | onError: (e) => {
31 | const errorMessage = e.data?.zodError?.fieldErrors.content;
32 | if (errorMessage && errorMessage[0]) {
33 | toast.error(errorMessage[0]);
34 | } else {
35 | toast.error("Failed to save! Please try again later.");
36 | }
37 | },
38 | });
39 |
40 | const toggleCodeBlock = () => {
41 | const instructionId = currentInstruction.id;
42 | if (!instructionId) return;
43 | updateInstruction({
44 | instructionId,
45 | hasCodeBlocks: !currentInstruction.hasCodeBlocks,
46 | });
47 | };
48 |
49 | const toggleDiff = () => setViewDiff(!viewDiff);
50 | return (
51 |
52 |
53 |
57 |
61 | Toggle diff view
62 |
63 |
64 | {isLoading ? (
65 |
66 | ) : (
67 |
68 |
73 |
77 | Toggle code blocks
78 |
79 |
80 | )}
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/ProjectsV2/ProjectTitleBlock.tsx:
--------------------------------------------------------------------------------
1 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
2 | import { CheckIcon, PencilIcon } from "@heroicons/react/24/solid";
3 | import { useRouter } from "next/router";
4 | import { api } from "~/utils/api";
5 | import { type KeyboardEvent, useState } from "react";
6 |
7 | type Props = {
8 | projectId: string;
9 | isEditingProject: boolean;
10 | projectTitle: string;
11 | };
12 | export default function ProjectTitleBlock({
13 | projectId,
14 | isEditingProject,
15 | projectTitle,
16 | }: Props) {
17 | const [isEditingTitle, setIsEditingTitle] = useState(false);
18 | const [title, setTitle] = useState(projectTitle);
19 | const router = useRouter();
20 |
21 | const { mutate: updateProjectTitle, isLoading: isUpdatingProjectTitle } =
22 | api.projects.update.useMutation({
23 | onSuccess: () => {
24 | void router.replace(router.asPath);
25 | setIsEditingTitle(false);
26 | },
27 | });
28 |
29 | function handleEnterKeyPress(f: () => void) {
30 | return handleKeyPress(f, "Enter");
31 | }
32 |
33 | function handleKeyPress(f: () => void, key: string) {
34 | return (e: KeyboardEvent) => {
35 | if (e.key === key) {
36 | f();
37 | }
38 | };
39 | }
40 |
41 | return (
42 |
43 | {isEditingTitle ? (
44 |
45 |
46 | setTitle(e.target.value)}
53 | onKeyDown={handleEnterKeyPress(() =>
54 | updateProjectTitle({
55 | projectId,
56 | title,
57 | })
58 | )}
59 | />
60 |
61 |
62 | ) : (
63 |
{projectTitle}
64 | )}
65 | {isUpdatingProjectTitle &&
}
66 | {isEditingTitle && !isUpdatingProjectTitle && (
67 |
68 | )}
69 | {isEditingProject && !isEditingTitle && !isUpdatingProjectTitle && (
70 |
71 |
setIsEditingTitle(true)}
74 | />
75 |
76 | )}
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/ProjectsV2/PurchaseNudge.tsx:
--------------------------------------------------------------------------------
1 | import EndOfTheRoad from "~/components/Images/EndOfTheRoad";
2 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
3 | import React, { useEffect, useState } from "react";
4 | import { SignUpButton, useUser } from "@clerk/nextjs";
5 | import { usePostHog } from "posthog-js/react";
6 | import { api } from "~/utils/api";
7 |
8 | type Props = {
9 | projectId: string;
10 | stripePriceId: string;
11 | instructionId: string;
12 | };
13 | export default function PurchaseNudge({
14 | projectId,
15 | stripePriceId,
16 | instructionId,
17 | }: Props) {
18 | const [isRedirectingToStripe, setIsRedirectingToStripe] = useState(false);
19 | const [redirectUrl, setRedirectUrl] = useState("");
20 | const postHog = usePostHog();
21 | const { user } = useUser();
22 |
23 | useEffect(() => {
24 | setRedirectUrl(window.location.href);
25 | }, []);
26 |
27 | const { data: price } = api.stripe.getPrices.useQuery(
28 | {
29 | priceId: stripePriceId,
30 | },
31 | {
32 | refetchOnWindowFocus: false,
33 | }
34 | );
35 |
36 | return (
37 |
40 |
41 |
42 | {" "}
43 | {"You've reached the end of your free preview."}{" "}
44 |
45 |
46 | {" "}
47 | {
48 | "To continue with the tutorial, you must purchase the full tutorial with the link below."
49 | }{" "}
50 |
51 |
52 | {user ? (
53 |
86 | ) : (
87 |
88 |
93 | Sign Up To Get The Coding Tutorial
94 |
95 |
96 | )}
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/ProjectsV2/QuestionsPurchaseNudge.tsx:
--------------------------------------------------------------------------------
1 | import EndOfTheRoad from "~/components/Images/EndOfTheRoad";
2 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
3 | import React, { useEffect, useState } from "react";
4 | import { SignUpButton, useUser } from "@clerk/nextjs";
5 | import { usePostHog } from "posthog-js/react";
6 | import { api } from "~/utils/api";
7 |
8 | type Props = {
9 | projectId: string;
10 | stripePriceId: string;
11 | instructionId: string;
12 | };
13 | export default function QuestionsPurchaseNudge({
14 | projectId,
15 | stripePriceId,
16 | instructionId,
17 | }: Props) {
18 | const [isRedirectingToStripe, setIsRedirectingToStripe] = useState(false);
19 | const postHog = usePostHog();
20 | const { user } = useUser();
21 | const [redirectUrl, setRedirectUrl] = useState("");
22 |
23 | useEffect(() => {
24 | setRedirectUrl(window.location.href);
25 | }, []);
26 |
27 | const { data: price } = api.stripe.getPrices.useQuery(
28 | {
29 | priceId: stripePriceId,
30 | },
31 | {
32 | refetchOnWindowFocus: false,
33 | }
34 | );
35 |
36 | return (
37 |
40 |
41 |
42 | {" "}
43 | {"To access the Q&A section, you must purchase the full tutorial."}{" "}
44 |
45 |
46 | {user ? (
47 |
80 | ) : (
81 |
82 |
87 | Sign Up To Get The Coding Tutorial
88 |
89 |
90 | )}
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/ProjectsV2/TableOfContentBlock.tsx:
--------------------------------------------------------------------------------
1 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
2 | import { CheckIcon, PencilIcon, XMarkIcon } from "@heroicons/react/24/solid";
3 | import { useRouter } from "next/router";
4 | import { api } from "~/utils/api";
5 | import { toast } from "react-hot-toast";
6 | import { type KeyboardEvent, useState } from "react";
7 |
8 | type Props = {
9 | entry: { id: string; title: string };
10 | instructionId: string;
11 | projectId: string;
12 | index: number;
13 | isEditingProject: boolean;
14 | };
15 | export default function TableOfContentBlock({
16 | entry,
17 | instructionId,
18 | projectId,
19 | index,
20 | isEditingProject,
21 | }: Props) {
22 | const [isEditingTitle, setIsEditingTitle] = useState(false);
23 | const [title, setTitle] = useState(entry.title);
24 | const router = useRouter();
25 |
26 | const { mutate: deleteInstruction, isLoading: isDeletingInstruction } =
27 | api.instructions.delete.useMutation({
28 | onSuccess: () => {
29 | void router.replace(router.asPath);
30 | },
31 | onError: (e) => {
32 | const errorMessage = e.data?.zodError?.fieldErrors.content;
33 | if (errorMessage && errorMessage[0]) {
34 | toast.error(errorMessage[0]);
35 | } else {
36 | toast.error(
37 | "Failed to create a new instruction. Please try again later."
38 | );
39 | }
40 | },
41 | });
42 |
43 | const { mutate: updateInstruction, isLoading: isUpdatingInstruction } =
44 | api.instructions.update.useMutation({
45 | onSuccess: () => {
46 | void router.replace(router.asPath);
47 | setIsEditingTitle(false);
48 | },
49 | });
50 |
51 | function handleEnterKeyPress(f: () => void) {
52 | return handleKeyPress(f, "Enter");
53 | }
54 |
55 | function handleKeyPress(f: () => void, key: string) {
56 | return (e: KeyboardEvent) => {
57 | if (e.key === key) {
58 | f();
59 | }
60 | };
61 | }
62 |
63 | return (
64 | {
70 | void (async () => {
71 | await router.push(
72 | `/projectsv2/${projectId}?instructionId=${entry.id}`
73 | );
74 | })();
75 | }}
76 | >
77 | {isEditingTitle ? (
78 |
79 |
80 | setTitle(e.target.value)}
87 | onKeyDown={handleEnterKeyPress(() =>
88 | updateInstruction({
89 | instructionId,
90 | title,
91 | })
92 | )}
93 | />
94 |
95 |
96 | ) : (
97 |
98 | {index + 1}. {entry.title}{" "}
99 |
100 | )}
101 | {(isDeletingInstruction || isUpdatingInstruction) && (
102 |
103 | )}
104 | {isEditingTitle && !isUpdatingInstruction && (
105 |
106 | )}
107 | {isEditingProject && !isEditingTitle && !isDeletingInstruction && (
108 |
109 |
setIsEditingTitle(true)}
112 | />
113 |
116 | deleteInstruction({
117 | id: entry.id,
118 | })
119 | }
120 | />
121 |
122 | )}
123 |
124 | );
125 | }
126 |
--------------------------------------------------------------------------------
/src/components/ProjectsV2/TextExplanationComponent.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { api } from "~/utils/api";
3 | import { toast } from "react-hot-toast";
4 | import { debounce } from "throttle-debounce";
5 | import "@uiw/react-md-editor/markdown-editor.css";
6 | import "@uiw/react-markdown-preview/markdown.css";
7 | import MdEditor from "~/components/Common/MdEditor/MdEditor";
8 |
9 | type Props = {
10 | instructionId: string;
11 | readOnly: boolean;
12 | isAuthor: boolean;
13 | initialExplanation: string;
14 | };
15 | export default function TextExplanationComponent({
16 | instructionId,
17 | readOnly,
18 | isAuthor,
19 | initialExplanation,
20 | }: Props) {
21 | const [explanation, setExplanation] = useState(
22 | initialExplanation
23 | );
24 | const [isInitialized, setIsInitialized] = useState(false);
25 |
26 | const debouncedSave = useCallback(
27 | debounce(1000, (instructionId: string, explanation: string) => {
28 | mutate({ instructionId, explanation });
29 | }),
30 | []
31 | );
32 |
33 | useEffect(() => {
34 | try {
35 | if (explanation && isAuthor) {
36 | debouncedSave(instructionId, explanation);
37 | }
38 | } catch (error: any) {}
39 | }, [explanation, debouncedSave]);
40 |
41 | const { mutate } = api.instructions.update.useMutation({
42 | onSuccess: () => {
43 | if (!isInitialized) {
44 | setIsInitialized(true);
45 | } else {
46 | // Show successfully saved state
47 | toast.success("Saved!");
48 | }
49 | },
50 | onError: (e) => {
51 | const errorMessage = e.data?.zodError?.fieldErrors.content;
52 | if (errorMessage && errorMessage[0]) {
53 | toast.error(errorMessage[0]);
54 | } else {
55 | toast.error("Failed to save! Please try again later.");
56 | }
57 | },
58 | });
59 |
60 | return (
61 |
65 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/QuestionsAndAnswers/CommentBox.tsx:
--------------------------------------------------------------------------------
1 | import MdEditor from "~/components/Common/MdEditor/MdEditor";
2 | import React, { useState } from "react";
3 | import { api } from "~/utils/api";
4 | import { toast } from "react-hot-toast";
5 | import { useUser } from "@clerk/nextjs";
6 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
7 |
8 | type Props = {
9 | parentCommentId?: string;
10 | questionId: string;
11 | cancelCallback?: () => void;
12 | };
13 | export default function CommentBox({
14 | parentCommentId,
15 | questionId,
16 | cancelCallback,
17 | }: Props) {
18 | const [comment, setComment] = useState("");
19 | const { user } = useUser();
20 | const userId = user?.id;
21 | const username = user?.username;
22 | const ctx = api.useContext();
23 |
24 | const { mutate, isLoading } = api.comments.create.useMutation({
25 | onSuccess: () => {
26 | setComment("");
27 | cancelCallback?.();
28 | void ctx.comments.getAllCommentsForQuestion.invalidate({ questionId });
29 | },
30 | onError: (e) => {
31 | const errorMessage = e.data?.zodError?.fieldErrors.content;
32 | if (errorMessage && errorMessage[0]) {
33 | toast.error(errorMessage[0]);
34 | } else {
35 | toast.error("Failed to ask the question — please try again later.");
36 | }
37 | },
38 | });
39 |
40 | const postComment = () => {
41 | if (comment && userId) {
42 | mutate({ userId, comment, parentCommentId, questionId, username });
43 | }
44 | };
45 |
46 | return (
47 |
51 |
52 | Comment
53 |
61 |
62 |
68 | {isLoading && (
69 |
70 | )}{" "}
71 | Add comment
72 |
73 |
74 | {cancelCallback && (
75 |
80 | Cancel
81 |
82 | )}
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/QuestionsAndAnswers/CommentsList.tsx:
--------------------------------------------------------------------------------
1 | import { api } from "~/utils/api";
2 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
3 | import EmptyComments from "~/components/Images/EmptyComments";
4 | import { getRelativeDate } from "~/utils/utils";
5 | import { useState } from "react";
6 | import CommentBox from "~/components/QuestionsAndAnswers/CommentBox";
7 |
8 | type Props = {
9 | questionId: string;
10 | };
11 | export default function CommentsList({ questionId }: Props) {
12 | const [commentToReplyTo, setCommentToReplyTo] = useState("");
13 | const { data, isFetching } = api.comments.getAllCommentsForQuestion.useQuery(
14 | {
15 | questionId,
16 | },
17 | {
18 | refetchOnWindowFocus: false,
19 | }
20 | );
21 |
22 | const cancelReplyToComment = () => setCommentToReplyTo("");
23 |
24 | if (isFetching)
25 | return (
26 |
27 | {" "}
28 | {" "}
29 |
30 | );
31 | if (!data) {
32 | return (
33 |
34 | {" "}
35 |
{" "}
36 |
No comments found...yet 👀
37 |
38 | );
39 | }
40 | return (
41 |
42 | {data.map((comment) => (
43 |
44 |
{comment.comment}
45 |
48 |
49 |
50 | {comment.username}
51 |
52 |
setCommentToReplyTo(comment.id)}>
53 | reply
54 |
55 |
56 |
57 | {getRelativeDate(comment.createdAt)}
58 |
59 |
60 | {commentToReplyTo === comment.id && (
61 |
62 |
67 |
68 | )}
69 | {comment.replies.map((reply) => (
70 | <>
71 |
75 |
{reply.comment}
76 |
81 |
setCommentToReplyTo(reply.id)}>
82 | reply
83 |
84 |
85 | {getRelativeDate(reply.createdAt)}
86 |
87 |
88 |
89 |
90 | {commentToReplyTo === reply.id && (
91 |
92 |
97 |
98 | )}
99 | >
100 | ))}
101 |
102 | ))}
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/QuestionsAndAnswers/QuestionAndAnswerComponent.tsx:
--------------------------------------------------------------------------------
1 | import QuestionBox from "~/components/QuestionsAndAnswers/QuestionBox";
2 | import QuestionsList from "~/components/QuestionsAndAnswers/QuestionsList";
3 | import FocusedQuestionComponent from "~/components/DraftProjects/FocusedQuestion";
4 | import { useState } from "react";
5 | import { type Questions } from "@prisma/client";
6 |
7 | type Props = {
8 | instructionId: string;
9 | projectId: string;
10 | };
11 | export default function QuestionAndAnswerComponent({
12 | instructionId,
13 | projectId,
14 | }: Props) {
15 | const [focusedQuestion, setFocusedQuestion] = useState();
16 |
17 | return (
18 |
19 | {" "}
20 | {focusedQuestion ? (
21 |
25 | ) : (
26 | <>
27 |
{" "}
28 |
29 |
33 | >
34 | )}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/QuestionsAndAnswers/QuestionBox.tsx:
--------------------------------------------------------------------------------
1 | import MdEditor from "~/components/Common/MdEditor/MdEditor";
2 | import React, { useState } from "react";
3 | import { api } from "~/utils/api";
4 | import { toast } from "react-hot-toast";
5 | import { SignedIn, SignedOut, SignUpButton, useUser } from "@clerk/nextjs";
6 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
7 |
8 | type Props = {
9 | instructionId: string;
10 | projectId: string;
11 | };
12 | export default function QuestionBox({ instructionId, projectId }: Props) {
13 | const [title, setTitle] = useState("");
14 | const [question, setQuestion] = useState("");
15 | const { user } = useUser();
16 | const userId = user?.id;
17 | const username = user?.username;
18 | const ctx = api.useContext();
19 |
20 | const { mutate, isLoading } = api.questions.create.useMutation({
21 | onSuccess: () => {
22 | setTitle("");
23 | setQuestion("");
24 | void ctx.questions.getAllQuestionsForInstruction.invalidate({
25 | instructionsId: instructionId,
26 | });
27 | },
28 | onError: (e) => {
29 | const errorMessage = e.data?.zodError?.fieldErrors.content;
30 | if (errorMessage && errorMessage[0]) {
31 | toast.error(errorMessage[0]);
32 | } else {
33 | toast.error("Failed to ask the question — please try again later.");
34 | }
35 | },
36 | });
37 |
38 | const postQuestion = () => {
39 | if (title && question && userId) {
40 | mutate({
41 | userId,
42 | instructionsId: instructionId,
43 | question,
44 | title,
45 | username,
46 | });
47 | }
48 | };
49 |
50 | return (
51 |
55 |
56 | {" "}
57 | Ask a question below{" "}
58 |
59 |
60 |
61 | {" "}
62 | Question Title:
63 |
64 | setTitle(e.target.value)}
68 | />
69 |
70 | {" "}
71 | Question Details:
72 |
73 |
81 |
82 |
83 |
89 | {isLoading && (
90 |
91 | )}{" "}
92 | Submit Question
93 |
94 |
95 |
96 |
100 |
105 | Sign Up To Ask A Question
106 |
107 |
108 |
109 |
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/src/components/QuestionsAndAnswers/QuestionsList.tsx:
--------------------------------------------------------------------------------
1 | import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
2 | import { getRelativeDate } from "~/utils/utils";
3 | import { api } from "~/utils/api";
4 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
5 | import { type Dispatch, type SetStateAction } from "react";
6 | import { type Questions } from "@prisma/client";
7 | import EmptyComments from "~/components/Images/EmptyComments";
8 |
9 | type Props = {
10 | instructionId: string;
11 | setFocusedQuestion: Dispatch>;
12 | };
13 | export default function QuestionsList({
14 | instructionId,
15 | setFocusedQuestion,
16 | }: Props) {
17 | const { data, isFetching } =
18 | api.questions.getAllQuestionsForInstruction.useQuery(
19 | {
20 | instructionsId: instructionId,
21 | },
22 | {
23 | refetchOnWindowFocus: false,
24 | }
25 | );
26 |
27 | if (isFetching) {
28 | return (
29 |
30 | {" "}
31 | {" "}
32 |
33 | );
34 | }
35 |
36 | if (!data || data.length === 0) {
37 | return (
38 |
39 |
No questions asked...yet 👀
40 |
41 | {" "}
42 | Ask the first question to get the discussion started{" "}
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | return (
50 |
51 | {" "}
52 | {data?.map((question) => (
53 |
setFocusedQuestion(question)}
59 | >
60 |
{question.title}
61 |
62 |
63 |
64 |
65 | {question._count.comments}
66 |
67 |
68 | {question.username}
69 |
70 |
71 |
72 | {getRelativeDate(question.createdAt)}
73 |
74 |
75 | ))}
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/env.mjs:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | /**
4 | * Specify your server-side environment variables schema here. This way you can ensure the app isn't
5 | * built with invalid env vars.
6 | */
7 | const server = z.object({
8 | DATABASE_URL: z.string().url(),
9 | NODE_ENV: z.enum(["development", "test", "production"]),
10 | });
11 |
12 | /**
13 | * Specify your client-side environment variables schema here. This way you can ensure the app isn't
14 | * built with invalid env vars. To expose them to the client, prefix them with `NEXT_PUBLIC_`.
15 | */
16 | const client = z.object({
17 | // NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
18 | });
19 |
20 | /**
21 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
22 | * middlewares) or client-side so we need to destruct manually.
23 | *
24 | * @type {Record | keyof z.infer, string | undefined>}
25 | */
26 | const processEnv = {
27 | DATABASE_URL: process.env.DATABASE_URL,
28 | NODE_ENV: process.env.NODE_ENV,
29 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
30 | };
31 |
32 | // Don't touch the part below
33 | // --------------------------
34 |
35 | const merged = server.merge(client);
36 |
37 | /** @typedef {z.input} MergedInput */
38 | /** @typedef {z.infer} MergedOutput */
39 | /** @typedef {z.SafeParseReturnType} MergedSafeParseReturn */
40 |
41 | let env = /** @type {MergedOutput} */ (process.env);
42 |
43 | if (!!process.env.SKIP_ENV_VALIDATION == false) {
44 | const isServer = typeof window === "undefined";
45 |
46 | const parsed = /** @type {MergedSafeParseReturn} */ (
47 | isServer
48 | ? merged.safeParse(processEnv) // on server we can validate all env vars
49 | : client.safeParse(processEnv) // on client we can only validate the ones that are exposed
50 | );
51 |
52 | if (parsed.success === false) {
53 | console.error(
54 | "❌ Invalid environment variables:",
55 | parsed.error.flatten().fieldErrors,
56 | );
57 | throw new Error("Invalid environment variables");
58 | }
59 |
60 | env = new Proxy(parsed.data, {
61 | get(target, prop) {
62 | if (typeof prop !== "string") return undefined;
63 | // Throw a descriptive error if a server-side env var is accessed on the client
64 | // Otherwise it would just be returning `undefined` and be annoying to debug
65 | if (!isServer && !prop.startsWith("NEXT_PUBLIC_"))
66 | throw new Error(
67 | process.env.NODE_ENV === "production"
68 | ? "❌ Attempted to access a server-side environment variable on the client"
69 | : `❌ Attempted to access server-side environment variable '${prop}' on the client`,
70 | );
71 | return target[/** @type {keyof typeof target} */ (prop)];
72 | },
73 | });
74 | }
75 |
76 | export { env };
77 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { withClerkMiddleware } from "@clerk/nextjs/server";
2 | import { NextResponse } from "next/server";
3 |
4 | export default withClerkMiddleware(() => {
5 | return NextResponse.next();
6 | });
7 |
8 | export const config = {
9 | matcher: "/((?!_next/image|_next/static|favicon.ico).*)",
10 | };
11 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { ClerkProvider } from "@clerk/nextjs";
2 | import { type AppType } from "next/app";
3 | import { api } from "~/utils/api";
4 | import "~/styles/globals.css";
5 | import { Analytics } from "@vercel/analytics/react";
6 | import { useEffect } from "react";
7 | import posthog from "posthog-js";
8 | import { PostHogProvider } from "posthog-js/react";
9 | import { useRouter } from "next/router";
10 | import Script from "next/script";
11 |
12 | if (typeof window !== "undefined" && process.env.NODE_ENV === "production") {
13 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? "", {
14 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://app.posthog.com",
15 | // Enable debug mode in development
16 | loaded: (posthog) => {
17 | if (process.env.NODE_ENV === "development") posthog.debug();
18 | },
19 | });
20 | }
21 |
22 | const MyApp: AppType = ({ Component, pageProps }) => {
23 | const router = useRouter();
24 | useEffect(() => {
25 | // Track page views
26 | const handleRouteChange = () => posthog?.capture("$pageview");
27 | router.events.on("routeChangeComplete", handleRouteChange);
28 |
29 | return () => {
30 | router.events.off("routeChangeComplete", handleRouteChange);
31 | };
32 | }, []);
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export { reportWebVitals } from "next-axiom";
45 | export default api.withTRPC(MyApp);
46 |
--------------------------------------------------------------------------------
/src/pages/api/checkout_sessions.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiResponse } from "next";
2 | import { type AxiomAPIRequest, withAxiom } from "next-axiom";
3 | import type Stripe from "stripe";
4 | import { PostHog } from "posthog-node";
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-var-requires,@typescript-eslint/no-unsafe-assignment
7 | const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
8 |
9 | type Data = {
10 | error?: string;
11 | data?: Stripe.Checkout.Session;
12 | };
13 |
14 | async function handler(req: AxiomAPIRequest, res: NextApiResponse) {
15 | if (req.method === "POST") {
16 | const { userId, projectId, stripePriceId, instructionId } = req.query as {
17 | userId: string;
18 | projectId: string;
19 | stripePriceId: string;
20 | instructionId: string;
21 | };
22 | try {
23 | req.log.info(`[api/checkout_sessions] Starting endpoint`, {
24 | userId,
25 | projectId,
26 | stripePriceId,
27 | instructionId,
28 | });
29 |
30 | const client = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? "", {
31 | host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://app.posthog.com",
32 | });
33 |
34 | const newProjectsUiEnabled =
35 | (await client.isFeatureEnabled("new-projects-ui", userId)) ?? true;
36 |
37 | let cancelUrl = `${
38 | process.env.NEXT_PUBLIC_BASE_URL ?? ""
39 | }/projects/preview/${projectId}?canceledPayment=true`;
40 |
41 | if (newProjectsUiEnabled) {
42 | cancelUrl = `${
43 | process.env.NEXT_PUBLIC_BASE_URL ?? ""
44 | }/projectsv2/${projectId}?instructionId=${instructionId}`;
45 | }
46 |
47 | await client.shutdownAsync();
48 | // Create Checkout Sessions from body params.
49 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
50 | const session: Stripe.Checkout.Session =
51 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
52 | await stripe.checkout.sessions.create({
53 | line_items: [
54 | {
55 | // Provide the exact Price ID (for example, pr_1234) of the product you want to sell
56 | price: stripePriceId,
57 | quantity: 1,
58 | },
59 | ],
60 | mode: "payment",
61 | success_url: `${
62 | process.env.NEXT_PUBLIC_BASE_URL ?? ""
63 | }/projects/successfulPurchase?userId=${userId}&projectId=${projectId}`,
64 | cancel_url: cancelUrl,
65 | automatic_tax: { enabled: true },
66 | });
67 |
68 | req.log.info(`[api/checkout_sessions] Completed endpoint`, {
69 | userId,
70 | projectId,
71 | stripePriceId,
72 | sessionUrl: session.url,
73 | });
74 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
75 | // @ts-ignore
76 | res.redirect(303, session.url);
77 | } catch (err: any) {
78 | req.log.error(`[api/checkout_sessions] Error`, {
79 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
80 | error: err.message,
81 | userId,
82 | projectId,
83 | stripePriceId,
84 | });
85 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
86 | res.status(err.statusCode || 500).json(err.message);
87 | }
88 | } else {
89 | res.setHeader("Allow", "POST");
90 | res.status(405).end("Method Not Allowed");
91 | }
92 | }
93 |
94 | export default withAxiom(handler);
95 |
--------------------------------------------------------------------------------
/src/pages/api/trpc/[trpc].ts:
--------------------------------------------------------------------------------
1 | import { createNextApiHandler } from "@trpc/server/adapters/next";
2 | import { env } from "~/env.mjs";
3 | import { createTRPCContext } from "~/server/api/trpc";
4 | import { appRouter } from "~/server/api/root";
5 | import { withAxiom } from "next-axiom";
6 |
7 | // export API handler
8 | export default withAxiom(
9 | createNextApiHandler({
10 | router: appRouter,
11 | createContext: createTRPCContext,
12 | onError:
13 | env.NODE_ENV === "development"
14 | ? ({ path, error }) => {
15 | console.error(
16 | `❌ tRPC failed on ${path ?? ""}: ${error.message}`
17 | );
18 | }
19 | : undefined,
20 | })
21 | );
22 |
--------------------------------------------------------------------------------
/src/pages/api/webhooks/clerk/users.tsx:
--------------------------------------------------------------------------------
1 | import clerk from "@clerk/clerk-sdk-node";
2 | import { Resend } from "resend";
3 | import { type IncomingHttpHeaders } from "http";
4 | import type { NextApiResponse } from "next";
5 | import { Webhook, type WebhookRequiredHeaders } from "svix";
6 | import { buffer } from "micro";
7 | import WelcomeEmail from "emails/WelcomeEmail";
8 | import { type AxiomAPIRequest, withAxiom } from "next-axiom";
9 | import MailerLite from "@mailerlite/mailerlite-nodejs";
10 |
11 | // Disable the bodyParser, so we can access the raw
12 | // request body for verification.
13 | export const config = {
14 | api: {
15 | bodyParser: false,
16 | },
17 | };
18 |
19 | type NextApiRequestWithSvixRequiredHeaders = AxiomAPIRequest & {
20 | headers: IncomingHttpHeaders & WebhookRequiredHeaders;
21 | };
22 |
23 | const webhookSecret: string = process.env.WEBHOOK_SECRET || "";
24 |
25 | const resend = new Resend(process.env.RESEND_API_KEY);
26 |
27 | const mailerlite = new MailerLite({
28 | api_key: process.env.MAILER_LITE_API_KEY ?? "",
29 | });
30 |
31 | async function handler(
32 | req: NextApiRequestWithSvixRequiredHeaders,
33 | res: NextApiResponse
34 | ) {
35 | // Verify the webhook signature
36 | // See https://docs.svix.com/receiving/verifying-payloads/how
37 | const payload = (await buffer(req)).toString();
38 | const headers = req.headers;
39 | const wh = new Webhook(webhookSecret);
40 | let evt: Event | null = null;
41 | try {
42 | evt = wh.verify(payload, headers) as Event;
43 |
44 | // Handle the webhook
45 | const eventType: EventType = evt.type;
46 | if (eventType === "user.created") {
47 | req.log.info("[api/webhooks/clerk/users] Starting endpoint", {
48 | data: JSON.stringify(evt.data),
49 | });
50 | const { id, email_addresses, first_name, last_name } = evt.data;
51 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
52 | if (!id || !email_addresses || !email_addresses[0].email_address) {
53 | req.log.info("[api/webhooks/clerk/users] Invalid data from Clerk", {
54 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
55 | data: JSON.stringify({ id, email_addresses }),
56 | });
57 | return;
58 | }
59 |
60 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
61 | const emailAddress = email_addresses[0].email_address as string;
62 |
63 | req.log.info("[api/webhooks/clerk/users] Sending welcome email", {
64 | data: JSON.stringify({
65 | id: id as string,
66 | emailAddress,
67 | first_name: first_name as string,
68 | last_name: last_name as string,
69 | }),
70 | });
71 |
72 | await resend.sendEmail({
73 | from: "noreply@updates.sweprojects.com",
74 | to: emailAddress,
75 | subject: "Welcome to SWE Projects! Here are some helpful links",
76 | react: ,
77 | });
78 |
79 | req.log.info("[api/webhooks/clerk/users] Adding user to mailer lite", {
80 | data: JSON.stringify({
81 | id: id as string,
82 | emailAddress,
83 | first_name: first_name as string,
84 | last_name: last_name as string,
85 | }),
86 | });
87 | const resp = await mailerlite.subscribers.createOrUpdate({
88 | email: emailAddress,
89 | fields: {
90 | name: first_name as string,
91 | last_name: last_name as string,
92 | clerk_id: id as string,
93 | },
94 | status: "active",
95 | });
96 |
97 | req.log.info("[api/webhooks/clerk/users] Updating clerk metadata", {
98 | data: JSON.stringify({
99 | id: id as string,
100 | emailAddress,
101 | first_name: first_name as string,
102 | last_name: last_name as string,
103 | }),
104 | });
105 | await clerk.users.updateUserMetadata(id as string, {
106 | privateMetadata: {
107 | mailerLiteId: resp.data.data.id,
108 | },
109 | });
110 |
111 | req.log.info("[api/webhooks/clerk/users] Completed endpoint", {
112 | data: JSON.stringify(evt.data),
113 | });
114 | res.json({});
115 | }
116 | } catch (e: any) {
117 | req.log.error(
118 | "[api/webhooks/clerk/users] Error",
119 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
120 | { error: e.message }
121 | );
122 | return res.status(400).json({});
123 | }
124 | }
125 |
126 | export default withAxiom(handler);
127 |
128 | // Generic (and naive) way for the Clerk event
129 | // payload type.
130 | type Event = {
131 | data: Record;
132 | object: "event";
133 | type: EventType;
134 | };
135 |
136 | type EventType = "user.created";
137 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { type NextPage } from "next";
2 | import Head from "next/head";
3 | import { CallToAction } from "~/components/LandingPage/CallToAction";
4 | import { Faqs } from "~/components/LandingPage/Faqs";
5 | import { Footer } from "~/components/LandingPage/Footer";
6 | import { Header } from "~/components/LandingPage/Header";
7 | import { Hero } from "~/components/LandingPage/Hero";
8 | import { PrimaryFeatures } from "~/components/LandingPage/PrimaryFeatures";
9 | import { Testimonials } from "~/components/LandingPage/Testimonials";
10 | import { useUser } from "@clerk/nextjs";
11 | import { useEffect } from "react";
12 | import { usePostHog } from "posthog-js/react";
13 |
14 | const Home: NextPage = () => {
15 | const { isSignedIn, user } = useUser();
16 | const postHog = usePostHog();
17 |
18 | useEffect(() => {
19 | if (isSignedIn && user) {
20 | postHog?.identify(user.id, {
21 | name: user.fullName,
22 | email: user.primaryEmailAddress?.emailAddress,
23 | });
24 | postHog?.capture("Visit Landing Page", {
25 | distinct_id: user.id,
26 | time: new Date(),
27 | });
28 | }
29 | }, [isSignedIn, user]);
30 |
31 | return (
32 | <>
33 |
34 |
35 | SWEProjects — Coding projects that teach you to become a better
36 | developer
37 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | >
57 | );
58 | };
59 |
60 | export default Home;
61 |
--------------------------------------------------------------------------------
/src/pages/my-projects.tsx:
--------------------------------------------------------------------------------
1 | import Header from "~/components/Common/Header";
2 | import React from "react";
3 | import BackgroundGradient from "~/components/Common/BackgroundGradient";
4 | import { getAuth } from "@clerk/nextjs/server";
5 | import { generateSSGHelper } from "~/server/helpers/ssgHelper";
6 | import { type GetServerSideProps } from "next";
7 | import { type CustomSessionClaims } from "~/utils/types";
8 | import { type Projects } from "@prisma/client";
9 | import { PostHog } from "posthog-node";
10 | import { useRouter } from "next/router";
11 | import { api } from "~/utils/api";
12 | import { PhotoIcon } from "@heroicons/react/24/solid";
13 | import LoadingSpinner from "~/components/Common/LoadingSpinner";
14 |
15 | type Props = {
16 | projects: Omit[];
17 | isNewProjectCreationFlowEnabled: boolean;
18 | };
19 | export default function MyProjects({
20 | isNewProjectCreationFlowEnabled,
21 | projects,
22 | }: Props) {
23 | const router = useRouter();
24 | if (!isNewProjectCreationFlowEnabled) {
25 | void router.push("/projects");
26 | }
27 |
28 | const { mutate: createNewProject, isLoading: isCreatingNewProject } =
29 | api.projects.create.useMutation({
30 | onSuccess: (data) => {
31 | void router.push(`/projectsv2/${data.id}`);
32 | },
33 | });
34 |
35 | return (
36 |
37 |
38 |
39 |
40 | {projects.length === 0 && (
41 |
42 | {
43 | "You haven't created a project tutorial yet. Create your first one below!"
44 | }
45 |
46 | )}
47 |
createNewProject()}
51 | >
52 | Create Project Tutorial
53 | {isCreatingNewProject && }
54 |
55 |
56 | {projects.map((project, index) => (
57 |
{
63 | void router.push(`/projectsv2/${project.id}`);
64 | }}
65 | >
66 |
71 |
72 | {project.title}
73 |
74 |
75 | ))}
76 |
77 |
78 |
79 | );
80 | }
81 |
82 | export const getServerSideProps: GetServerSideProps = async (
83 | context
84 | ) => {
85 | const session = getAuth(context.req);
86 | const userId = session.userId;
87 | const isAdmin =
88 | (session.sessionClaims as CustomSessionClaims)?.publicMetadata?.isAdmin ??
89 | false;
90 | let projects: Projects[] = [];
91 | if (userId) {
92 | const ssg = generateSSGHelper(userId ?? undefined, isAdmin);
93 | projects =
94 | (await ssg.projects.getUsersCreatedProjects.fetch({ userId })) ?? [];
95 | }
96 |
97 | let isNewProjectCreationFlowEnabled = false;
98 | if (userId) {
99 | const client = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? "", {
100 | host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://app.posthog.com",
101 | });
102 |
103 | isNewProjectCreationFlowEnabled =
104 | (await client.isFeatureEnabled("new-projects-creation-flow", userId)) ??
105 | false;
106 |
107 | await client.shutdownAsync();
108 | }
109 |
110 | return {
111 | props: {
112 | projects: projects
113 | .reverse()
114 | .map((project) => ({ ...project, createdAt: null })),
115 | isNewProjectCreationFlowEnabled,
116 | },
117 | };
118 | };
119 |
--------------------------------------------------------------------------------
/src/pages/projects/successfulPurchase.tsx:
--------------------------------------------------------------------------------
1 | import PersonCoding from "~/components/Images/PersonCoding";
2 | import Link from "next/link";
3 |
4 | export default function SuccessfulPurchase() {
5 | return (
6 | <>
7 |
12 |
21 |
22 | {" "}
23 | Congrats on purchasing the project!{" "}
24 |
25 |
26 | {`Once we finish processing your payment,
27 | you will receive an email confirmation
28 | with a direct link to the project.`}
29 |
30 |
31 |
35 | View All Of Your Projects
36 |
37 |
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/pages/projectsv2/[projectId]/completed.tsx:
--------------------------------------------------------------------------------
1 | import Celebration from "~/components/Images/Celebration";
2 | import { usePostHog } from "posthog-js/react";
3 | import { useEffect } from "react";
4 | import { useRouter } from "next/router";
5 | import { useUser } from "@clerk/nextjs";
6 | import Link from "next/link";
7 | import Header from "~/components/Common/Header";
8 |
9 | export default function CompletedPage() {
10 | const router = useRouter();
11 | const { projectId } = router.query as {
12 | projectId: string;
13 | };
14 | const { isSignedIn, user } = useUser();
15 | const postHog = usePostHog();
16 |
17 | useEffect(() => {
18 | if (
19 | process.env.NODE_ENV === "production" &&
20 | isSignedIn &&
21 | user &&
22 | projectId
23 | ) {
24 | postHog?.identify(user?.id, {
25 | name: user?.fullName,
26 | email: user?.primaryEmailAddress?.emailAddress,
27 | });
28 | postHog?.capture("Successfully Completed Project", {
29 | distinct_id: user.id,
30 | project_id: projectId,
31 | time: new Date(),
32 | });
33 | }
34 | }, [isSignedIn, projectId, user, postHog]);
35 | return (
36 |
37 |
38 |
39 |
{" "}
40 |
41 | {" "}
42 | Congrats on finishing the project tutorial!{" "}
43 |
44 |
45 |
49 | {" "}
50 | Click here to go back to view all the projects{" "}
51 |
52 |
53 |
{" "}
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/pages/sign-in/[[...index]].tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 |
3 | const SignInPage = () => (
4 |
5 | );
6 | export default SignInPage;
7 |
--------------------------------------------------------------------------------
/src/pages/sign-up/[[...index]].tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 |
3 | const SignUpPage = () => (
4 |
5 | );
6 | export default SignUpPage;
7 |
--------------------------------------------------------------------------------
/src/server/api/root.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCRouter } from "~/server/api/trpc";
2 | import { projectsRouter } from "~/server/api/routers/projects";
3 | import { instructionsRouter } from "~/server/api/routers/instructions";
4 | import { codeBlocksRouter } from "~/server/api/routers/codeBlocks";
5 | import { purchasesRouter } from "~/server/api/routers/purchases";
6 | import { stripeRouter } from "~/server/api/routers/stripe";
7 | import { questionsRouter } from "~/server/api/routers/questions";
8 | import { commentsRouter } from "~/server/api/routers/comments";
9 | import { projectPreviewEnrollmentsRouter } from "~/server/api/routers/projectPreviewEnrollments";
10 |
11 | /**
12 | * This is the primary router for your server.
13 | *
14 | * All routers added in /api/routers should be manually added here.
15 | */
16 | export const appRouter = createTRPCRouter({
17 | projects: projectsRouter,
18 | instructions: instructionsRouter,
19 | codeBlocks: codeBlocksRouter,
20 | comments: commentsRouter,
21 | purchases: purchasesRouter,
22 | questions: questionsRouter,
23 | stripe: stripeRouter,
24 | projectPreviewEnrollments: projectPreviewEnrollmentsRouter,
25 | });
26 |
27 | // export type definition of API
28 | export type AppRouter = typeof appRouter;
29 |
--------------------------------------------------------------------------------
/src/server/api/routers/comments.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCRouter, privateProcedure } from "~/server/api/trpc";
2 | import { z } from "zod";
3 |
4 | export const commentsRouter = createTRPCRouter({
5 | create: privateProcedure
6 | .input(
7 | z.object({
8 | userId: z.string(),
9 | questionId: z.string(),
10 | comment: z.string(),
11 | parentCommentId: z.string().optional(),
12 | username: z.string().nullable().optional(),
13 | })
14 | )
15 | .mutation(async ({ ctx, input }) => {
16 | try {
17 | ctx.log?.info("[comments] Starting endpoint", {
18 | userId: ctx.userId,
19 | function: "create",
20 | input: JSON.stringify(input),
21 | });
22 | const { userId, questionId, comment, parentCommentId, username } =
23 | input;
24 |
25 | const result = await ctx.prisma.comment.create({
26 | data: {
27 | userId,
28 | questionId,
29 | comment,
30 | parentCommentId,
31 | username,
32 | },
33 | });
34 |
35 | ctx.log?.info("[comments] Completed endpoint", {
36 | userId: ctx.userId,
37 | function: "create",
38 | input: JSON.stringify(input),
39 | result: JSON.stringify(result),
40 | });
41 |
42 | return result;
43 | } catch (error) {
44 | ctx.log?.error("[comments] Failed endpoint", {
45 | userId: ctx.userId,
46 | function: "create",
47 | input: JSON.stringify(input),
48 | error: JSON.stringify(error),
49 | });
50 | throw error;
51 | }
52 | }),
53 | getAllCommentsForQuestion: privateProcedure
54 | .input(z.object({ questionId: z.string() }))
55 | .query(async ({ ctx, input }) => {
56 | try {
57 | ctx.log?.info("[comments] Starting endpoint", {
58 | userId: ctx.userId,
59 | function: "getAllCommentsForQuestion",
60 | input: JSON.stringify(input),
61 | });
62 |
63 | const { questionId } = input;
64 |
65 | const result = await ctx.prisma.comment.findMany({
66 | where: {
67 | questionId,
68 | parentCommentId: null,
69 | },
70 | include: {
71 | replies: true,
72 | },
73 | orderBy: {
74 | createdAt: "desc",
75 | },
76 | });
77 |
78 | ctx.log?.info("[comments] Completed endpoint", {
79 | userId: ctx.userId,
80 | function: "getAllCommentsForQuestion",
81 | input: JSON.stringify(input),
82 | result: JSON.stringify(result),
83 | });
84 |
85 | return result;
86 | } catch (error) {
87 | ctx.log?.error("[comments] Failed endpoint", {
88 | userId: ctx.userId,
89 | function: "getAllCommentsForQuestion",
90 | input: JSON.stringify(input),
91 | error: JSON.stringify(error),
92 | });
93 | throw error;
94 | }
95 | }),
96 | });
97 |
--------------------------------------------------------------------------------
/src/server/api/routers/projectPreviewEnrollments.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createTRPCRouter,
3 | privateProcedure,
4 | publicProcedure,
5 | } from "~/server/api/trpc";
6 | import { z } from "zod";
7 |
8 | export const projectPreviewEnrollmentsRouter = createTRPCRouter({
9 | create: privateProcedure
10 | .input(
11 | z.object({
12 | email: z.string(),
13 | projectsId: z.string(),
14 | userId: z.string(),
15 | })
16 | )
17 | .mutation(async ({ ctx, input }) => {
18 | try {
19 | ctx.log?.info("[projectPreviewEnrollments] Starting endpoint", {
20 | userId: ctx.userId,
21 | function: "create",
22 | input: JSON.stringify(input),
23 | });
24 | const { userId, projectsId, email } = input;
25 |
26 | const result = await ctx.prisma.projectPreviewEnrollment.create({
27 | data: {
28 | userId,
29 | projectsId,
30 | email,
31 | },
32 | });
33 |
34 | ctx.log?.info("[projectPreviewEnrollments] Completed endpoint", {
35 | userId: ctx.userId,
36 | function: "create",
37 | input: JSON.stringify(input),
38 | result: JSON.stringify(result),
39 | });
40 |
41 | return result;
42 | } catch (error) {
43 | ctx.log?.error("[projectPreviewEnrollments] Failed endpoint", {
44 | userId: ctx.userId,
45 | function: "create",
46 | input: JSON.stringify(input),
47 | error: JSON.stringify(error),
48 | });
49 | throw error;
50 | }
51 | }),
52 | getUsersProjectPreviewEnrollmentsForProjectId: publicProcedure
53 | .input(
54 | z.object({
55 | userId: z.string().nullable().optional(),
56 | projectsId: z.string(),
57 | })
58 | )
59 | .query(async ({ ctx, input }) => {
60 | try {
61 | ctx.log?.info("[projectPreviewEnrollments] Starting endpoint", {
62 | userId: ctx.userId,
63 | function: "getUsersProjectPreviewEnrollmentsForProjectId",
64 | input: JSON.stringify(input),
65 | });
66 |
67 | const { projectsId, userId } = input;
68 |
69 | if (!userId) return [];
70 |
71 | const result = await ctx.prisma.projectPreviewEnrollment.findMany({
72 | where: {
73 | projectsId,
74 | userId,
75 | },
76 | });
77 |
78 | ctx.log?.info("[projectPreviewEnrollments] Completed endpoint", {
79 | userId: ctx.userId,
80 | function: "getUsersProjectPreviewEnrollmentsForProjectId",
81 | input: JSON.stringify(input),
82 | result: JSON.stringify(result),
83 | });
84 |
85 | return result;
86 | } catch (error) {
87 | ctx.log?.error("[projectPreviewEnrollments] Failed endpoint", {
88 | userId: ctx.userId,
89 | function: "getUsersProjectPreviewEnrollmentsForProjectId",
90 | input: JSON.stringify(input),
91 | error: JSON.stringify(error),
92 | });
93 | throw error;
94 | }
95 | }),
96 | });
97 |
--------------------------------------------------------------------------------
/src/server/api/routers/purchases.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCRouter, privateProcedure } from "~/server/api/trpc";
2 | import { z } from "zod";
3 |
4 | export const purchasesRouter = createTRPCRouter({
5 | create: privateProcedure
6 | .input(z.object({ projectsId: z.string(), userId: z.string() }))
7 | .mutation(async ({ ctx, input }) => {
8 | try {
9 | const { projectsId, userId } = input;
10 | ctx.log?.info("[purchases] Starting endpoint", {
11 | userId: ctx.userId,
12 | function: "create",
13 | input: JSON.stringify(input),
14 | });
15 |
16 | const result = await ctx.prisma.purchases.create({
17 | data: {
18 | projectsId,
19 | userId,
20 | },
21 | });
22 |
23 | ctx.log?.info("[purchases] Completed endpoint", {
24 | userId: ctx.userId,
25 | function: "create",
26 | input: JSON.stringify(input),
27 | result: JSON.stringify(result),
28 | });
29 | return result;
30 | } catch (error) {
31 | ctx.log?.error("[purchases] Failed endpoint", {
32 | userId: ctx.userId,
33 | function: "create",
34 | input: JSON.stringify(input),
35 | error: JSON.stringify(error),
36 | });
37 | throw error;
38 | }
39 | }),
40 | });
41 |
--------------------------------------------------------------------------------
/src/server/api/routers/questions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createTRPCRouter,
3 | privateProcedure,
4 | publicProcedure,
5 | } from "~/server/api/trpc";
6 | import { z } from "zod";
7 |
8 | export const questionsRouter = createTRPCRouter({
9 | create: privateProcedure
10 | .input(
11 | z.object({
12 | instructionsId: z.string(),
13 | userId: z.string(),
14 | question: z.string(),
15 | title: z.string(),
16 | username: z.string().nullable().optional(),
17 | })
18 | )
19 | .mutation(async ({ ctx, input }) => {
20 | try {
21 | ctx.log?.info("[questions] Starting endpoint", {
22 | userId: ctx.userId,
23 | function: "create",
24 | input: JSON.stringify(input),
25 | });
26 | const { instructionsId, userId, question, title, username } = input;
27 |
28 | const result = await ctx.prisma.questions.create({
29 | data: {
30 | instructionsId,
31 | userId,
32 | question,
33 | title,
34 | username,
35 | },
36 | });
37 |
38 | ctx.log?.info("[questions] Completed endpoint", {
39 | userId: ctx.userId,
40 | function: "create",
41 | input: JSON.stringify(input),
42 | result: JSON.stringify(result),
43 | });
44 |
45 | return result;
46 | } catch (error) {
47 | ctx.log?.error("[questions] Failed endpoint", {
48 | userId: ctx.userId,
49 | function: "create",
50 | input: JSON.stringify(input),
51 | error: JSON.stringify(error),
52 | });
53 | throw error;
54 | }
55 | }),
56 | getAllQuestionsForInstruction: publicProcedure
57 | .input(z.object({ instructionsId: z.string() }))
58 | .query(async ({ ctx, input }) => {
59 | try {
60 | ctx.log?.info("[questions] Starting endpoint", {
61 | userId: ctx.userId,
62 | function: "getQuestionsForInstruction",
63 | input: JSON.stringify(input),
64 | });
65 |
66 | const { instructionsId } = input;
67 |
68 | const result = await ctx.prisma.questions.findMany({
69 | where: {
70 | instructionsId,
71 | },
72 | include: {
73 | _count: {
74 | select: { comments: true },
75 | },
76 | },
77 | orderBy: {
78 | createdAt: "desc",
79 | },
80 | });
81 |
82 | ctx.log?.info("[questions] Completed endpoint", {
83 | userId: ctx.userId,
84 | function: "getQuestionsForInstruction",
85 | input: JSON.stringify(input),
86 | result: JSON.stringify(result),
87 | });
88 |
89 | return result;
90 | } catch (error) {
91 | ctx.log?.error("[questions] Failed endpoint", {
92 | userId: ctx.userId,
93 | function: "getQuestionsForInstruction",
94 | input: JSON.stringify(input),
95 | error: JSON.stringify(error),
96 | });
97 | throw error;
98 | }
99 | }),
100 | });
101 |
--------------------------------------------------------------------------------
/src/server/api/routers/stripe.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 | import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
3 | import { z } from "zod";
4 |
5 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "", {
6 | apiVersion: "2022-11-15",
7 | });
8 | export const stripeRouter = createTRPCRouter({
9 | getPrices: publicProcedure
10 | .input(
11 | z.object({
12 | priceId: z.string().optional(),
13 | })
14 | )
15 | .query(async ({ ctx, input }) => {
16 | try {
17 | ctx.log?.info("[stripe] Starting endpoint", {
18 | function: "getPrices",
19 | input: JSON.stringify(input),
20 | });
21 | const { priceId } = input;
22 | if (!priceId) return;
23 | const price = await stripe.prices.retrieve(priceId);
24 | if (!price.unit_amount) return;
25 | ctx.log?.info("[stripe] Completed endpoint", {
26 | function: "getPrices",
27 | input: JSON.stringify(input),
28 | price: price.unit_amount / 100,
29 | });
30 | return price.unit_amount / 100;
31 | } catch (error) {
32 | ctx.log?.error("[stripe] Failed endpoint", {
33 | function: "getPrices",
34 | input: JSON.stringify(input),
35 | error: JSON.stringify(error),
36 | });
37 | throw error;
38 | }
39 | }),
40 | });
41 |
--------------------------------------------------------------------------------
/src/server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | import { env } from "~/env.mjs";
4 |
5 | const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
6 |
7 | export const prisma =
8 | globalForPrisma.prisma ||
9 | new PrismaClient({
10 | log:
11 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
12 | });
13 |
14 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
15 |
--------------------------------------------------------------------------------
/src/server/helpers/ssgHelper.ts:
--------------------------------------------------------------------------------
1 | import { createProxySSGHelpers } from "@trpc/react-query/ssg";
2 | import { appRouter } from "~/server/api/root";
3 | import superjson from "superjson";
4 | import { createInnerTRPCContext } from "~/server/api/trpc";
5 |
6 | export const generateSSGHelper = (userId?: string, isAdmin?: boolean) =>
7 | createProxySSGHelpers({
8 | router: appRouter,
9 | ctx: createInnerTRPCContext(userId, isAdmin),
10 | transformer: superjson, // optional - adds superjson serialization
11 | });
12 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/utils/api.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which
3 | * contains the Next.js App-wrapper, as well as your type-safe React Query hooks.
4 | *
5 | * We also create a few inference helpers for input and output types.
6 | */
7 | import { httpBatchLink, loggerLink } from "@trpc/client";
8 | import { createTRPCNext } from "@trpc/next";
9 | import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
10 | import superjson from "superjson";
11 |
12 | import { type AppRouter } from "~/server/api/root";
13 |
14 | const getBaseUrl = () => {
15 | if (typeof window !== "undefined") return ""; // browser should use relative url
16 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
17 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
18 | };
19 |
20 | /** A set of type-safe react-query hooks for your tRPC API. */
21 | export const api = createTRPCNext({
22 | config() {
23 | return {
24 | /**
25 | * Transformer used for data de-serialization from the server.
26 | *
27 | * @see https://trpc.io/docs/data-transformers
28 | */
29 | transformer: superjson,
30 |
31 | /**
32 | * Links used to determine request flow from client to server.
33 | *
34 | * @see https://trpc.io/docs/links
35 | */
36 | links: [
37 | loggerLink({
38 | enabled: (opts) =>
39 | process.env.NODE_ENV === "development" ||
40 | (opts.direction === "down" && opts.result instanceof Error),
41 | }),
42 | httpBatchLink({
43 | url: `${getBaseUrl()}/api/trpc`,
44 | }),
45 | ],
46 | };
47 | },
48 | /**
49 | * Whether tRPC should await queries when server rendering pages.
50 | *
51 | * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
52 | */
53 | ssr: false,
54 | });
55 |
56 | /**
57 | * Inference helper for inputs.
58 | *
59 | * @example type HelloInput = RouterInputs['example']['hello']
60 | */
61 | export type RouterInputs = inferRouterInputs;
62 |
63 | /**
64 | * Inference helper for outputs.
65 | *
66 | * @example type HelloOutput = RouterOutputs['example']['hello']
67 | */
68 | export type RouterOutputs = inferRouterOutputs;
69 |
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | import { type JwtPayload } from "@clerk/types";
2 |
3 | export type CustomSessionClaims = JwtPayload & {
4 | publicMetadata: {
5 | isAdmin: boolean;
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export function wait(number: number) {
2 | return new Promise((resolve) => setTimeout(resolve, number));
3 | }
4 |
5 | export function getRelativeDate(date: Date) {
6 | const now = new Date();
7 | const diff = now.getTime() - date.getTime();
8 |
9 | const minute = 60 * 1000;
10 | const hour = minute * 60;
11 | const day = hour * 24;
12 | const week = day * 7;
13 | const month = day * 30;
14 | const year = day * 365;
15 |
16 | if (diff < minute) {
17 | return "just now";
18 | } else if (diff < hour) {
19 | const minutesAgo = Math.floor(diff / minute);
20 | return `${minutesAgo} minute${minutesAgo > 1 ? "s" : ""} ago`;
21 | } else if (diff < day) {
22 | const hoursAgo = Math.floor(diff / hour);
23 | return `${hoursAgo} hour${hoursAgo > 1 ? "s" : ""} ago`;
24 | } else if (diff < week) {
25 | const daysAgo = Math.floor(diff / day);
26 | return `${daysAgo} day${daysAgo > 1 ? "s" : ""} ago`;
27 | } else if (diff < month) {
28 | const weeksAgo = Math.floor(diff / week);
29 | return `${weeksAgo} week${weeksAgo > 1 ? "s" : ""} ago`;
30 | } else if (diff < year) {
31 | const monthsAgo = Math.floor(diff / month);
32 | return `${monthsAgo} month${monthsAgo > 1 ? "s" : ""} ago`;
33 | } else {
34 | const yearsAgo = Math.floor(diff / year);
35 | return `${yearsAgo} year${yearsAgo > 1 ? "s" : ""} ago`;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { type Config } from "tailwindcss";
2 |
3 | export default {
4 | content: ["./src/**/*.{js,ts,jsx,tsx}"],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | } satisfies Config;
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "checkJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "noUncheckedIndexedAccess": true,
19 | "baseUrl": ".",
20 | "paths": {
21 | "~/*": ["./src/*"]
22 | }
23 | },
24 | "include": [
25 | ".eslintrc.cjs",
26 | "next-env.d.ts",
27 | "**/*.ts",
28 | "**/*.tsx",
29 | "**/*.cjs",
30 | "**/*.mjs"
31 | ],
32 | "exclude": ["node_modules"]
33 | }
34 |
--------------------------------------------------------------------------------