diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..c802966 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,62 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..0c83ac4 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..92fbab5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +# Ignore artifacts: +build +coverage +node_modules + +# Ignore all HTML files +*.html \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a0b9a18 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "avoid", + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "css", + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/README.md b/README.md index c403366..3cbd223 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,2 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +# Quickgrade +Efforlessly check your grade online. \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index b677ac4..b0f135b 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..0db431f --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 4678774..e225cde 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + experimental: { + reactCompiler: true, + } +}; export default nextConfig; diff --git a/package.json b/package.json index 0fbe3b1..e28e2f2 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,32 @@ "lint": "next lint" }, "dependencies": { - "react": "^18", - "react-dom": "^18", - "next": "14.2.3" + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", + "babel-plugin-react-compiler": "^0.0.0-experimental-938cd9a-20240601", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "input-otp": "^1.2.4", + "lucide-react": "^0.390.0", + "next": "^15.0.0-rc.0", + "prettier": "^3.3.1", + "react": "^19.0.0-rc-eb259b5d3b-20240605", + "react-dom": "^19.0.0-rc-eb259b5d3b-20240605", + "shadcn-ui": "^0.8.0", + "tailwind-merge": "^2.3.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.3", "postcss": "^8", + "prettier-plugin-tailwindcss": "^0.6.1", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.3" + "typescript": "^5" } } diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f8422..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/globals.css b/src/app/globals.css index 875c01e..867b164 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,33 +1,76 @@ @tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -@layer utilities { - .text-balance { - text-wrap: balance; + @tailwind components; + @tailwind utilities; + + @layer base { + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + + --primary: 24 9.8% 10%; + --primary-foreground: 60 9.1% 97.8%; + + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 20 14.3% 4.1%; + + --radius: 0.5rem; + } + + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + + --primary: 60 9.1% 97.8%; + --primary-foreground: 24 9.8% 10%; + + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 24 5.7% 82.9%; + } } -} + + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + } \ No newline at end of file diff --git a/src/app/icon.png b/src/app/icon.png new file mode 100644 index 0000000..04e3fe4 Binary files /dev/null and b/src/app/icon.png differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3314e47..2ed14e6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,13 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import './globals.css'; +import { GradeProvider } from '@/components/gradeContext'; -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: 'Quickgrade', + description: 'Effortlessly check your average grade with a few clicks.', }; export default function RootLayout({ @@ -16,7 +17,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 2acfd44..b7a1fc7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,113 +1,14 @@ -import Image from "next/image"; +import { Label } from '@/components/ui/label'; +import GradeCounter from '@/components/gradeCounter'; export default function Home() { return ( -
-
-

- Get started by editing  - src/app/page.tsx -

-
- - By{" "} - Vercel Logo - -
-
- -
- Next.js Logo -
- -
- -

- Docs{" "} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{" "} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{" "} - - -> - -

-

- Explore starter templates for Next.js. -

-
- - -

- Deploy{" "} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
+
+ + +
); } diff --git a/src/components/gradeContext.tsx b/src/components/gradeContext.tsx new file mode 100644 index 0000000..dc13d36 --- /dev/null +++ b/src/components/gradeContext.tsx @@ -0,0 +1,35 @@ +'use client'; +import React, { + createContext, + useState, + useContext, + PropsWithChildren, +} from 'react'; + +interface GradeContextProps { + grades: number[]; + setGrades: React.Dispatch>; + grade: string; + setGrade: React.Dispatch>; +} + +const GradeContext = createContext({} as GradeContextProps); + +export const GradeProvider = ({ children }: PropsWithChildren) => { + const [grades, setGrades] = useState([]); + const [grade, setGrade] = useState(''); + + return ( + + {children} + + ); +}; + +export const useGradeContext = () => { + const context = useContext(GradeContext); + if (context === undefined) { + throw new Error('useGradeContext must be used within a GradeProvider'); + } + return context; +}; diff --git a/src/components/gradeCounter.tsx b/src/components/gradeCounter.tsx new file mode 100644 index 0000000..5937c17 --- /dev/null +++ b/src/components/gradeCounter.tsx @@ -0,0 +1,66 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { InputOTP, InputOTPSlot } from '@/components/ui/input-otp'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import GradeSlot from '@/components/gradeSlot'; +import { useGradeContext } from '@/components/gradeContext'; + +const GradeCounter = () => { + const { grade, grades, setGrades, setGrade } = useGradeContext(); + const [average, setAverage] = useState(''); + const addGrade = (grade: string | null) => { + if (typeof grade == null) { + setGrade(''); + return; + } + if (typeof grade === 'string') { + setGrades(prevGrades => [...prevGrades, parseInt(grade)]); + setGrade(''); + } + }; + + const calculateAverage = () => { + const sum = grades.reduce((a, b) => a + b, 0); + return grades.length ? (sum / grades.length).toFixed(2) : 'No grades yet'; + }; + + useEffect(() => { + setAverage(calculateAverage()); + }, [grades]); + + return ( +
+
+ setGrade(e)} + value={grade} + > + + + +
+
+ +
+ {grades.map(grade => { + return ; + })} +
+
+ + +
+ ); +}; + +export default GradeCounter; diff --git a/src/components/gradeSlot.tsx b/src/components/gradeSlot.tsx new file mode 100644 index 0000000..49634c9 --- /dev/null +++ b/src/components/gradeSlot.tsx @@ -0,0 +1,28 @@ +'use client'; +import { Label } from '@/components/ui/label'; +import { X } from 'lucide-react'; +import { useGradeContext } from '@/components/gradeContext'; +export default function GradeSlot({ grade }: { grade: number }) { + const { setGrades, grades } = useGradeContext(); + + const onDelete = () => { + const index = grades.findIndex(g => g === grade); + if (index !== -1) { + const newGrades = [...grades]; + newGrades.splice(index, 1); + setGrades(newGrades); + } + }; + + return ( +
+ +
+ +
+
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..0270f64 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx new file mode 100644 index 0000000..84d0720 --- /dev/null +++ b/src/components/ui/input-otp.tsx @@ -0,0 +1,71 @@ +"use client" + +import * as React from "react" +import { DashIcon } from "@radix-ui/react-icons" +import { OTPInput, OTPInputContext } from "input-otp" + +import { cn } from "@/lib/utils" + +const InputOTP = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( +
+)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext) + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( +
+ +
+)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..4fc3b47 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/tailwind.config.ts b/tailwind.config.ts index e9a0944..d97527c 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,20 +1,86 @@ -import type { Config } from "tailwindcss"; +import type { Config } from "tailwindcss" -const config: Config = { +const config = { + darkMode: ["class"], content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + "caret-blink": { + "0%,70%,100%": { opacity: "1" }, + "20%,50%": { opacity: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + "caret-blink": "caret-blink 1.25s ease-out infinite", + }, }, }, - plugins: [], -}; -export default config; + plugins: [require("tailwindcss-animate")], +} satisfies Config + +export default config \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 7b28589..f48e7ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -18,9 +22,19 @@ } ], "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }