Add Initial UI and Authentication (#11)

This commit was merged in pull request #11.
This commit is contained in:
Nicholas Pease
2024-02-20 14:34:40 -05:00
committed by GitHub
38 changed files with 2533 additions and 71 deletions
+1 -4
View File
@@ -18,12 +18,9 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 21.6.2
- name: Install dependencies
run: npm install frontend-next/
- name: Lint
run: npm --prefix frontend-next/ run lint
- name: Build Frontend
run: npm --prefix frontend-next/ run build
+4
View File
@@ -1,3 +1,7 @@
# Firebase Stuff
firebase-admin-key.json
firebase*.json
# Logs
logs
*.log
+1 -1
View File
@@ -1,4 +1,4 @@
# ChatMaps
![](/frontend-next/public/logos/logo_transparent.png)
Main repo for ChatMaps, our COS420 Project.
ChatMaps is a web-based social networking service that allows users to connect to others in their local geographic area. It will implement an interactable mapping utility to show general user locations relative to other users, as well as a chat room feature that allows users to start public conversations based on a specified topic. ChatMaps is primarily intended for use in densely populated areas, such as college campuses or metropolitan areas, so people of similar interests can start conversations. The goal of this project is to create a web app that plots locations, gives a radius of the local area, and connects users into different topic-based chat rooms.
+1866 -24
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -9,9 +9,13 @@
"lint": "next lint"
},
"dependencies": {
"firebase": "^10.8.0",
"firebase-admin": "^12.0.0",
"next": "^14.1.0",
"pigeon-maps": "^0.21.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-hook-form": "^7.50.1"
},
"devDependencies": {
"autoprefixer": "^10.0.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 629 B

@@ -0,0 +1,11 @@
// lib/firebase-admin-config.js
import { initializeApp, getApps, cert } from "firebase-admin/app";
import serviceAccount from "../../../../firebase-admin-key"
export function customInitApp() {
if (getApps().length <= 0) {
initializeApp({
credential: cert(serviceAccount)
});
}
}
@@ -0,0 +1,10 @@
import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import firebaseConfigFile from "../../../../firebase-config"
var firebaseConfig = firebaseConfigFile;
var app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);
var auth = getAuth(app);
export { auth, app };
+64
View File
@@ -0,0 +1,64 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
// Firebase Imports
import { auth } from "firebase-admin";
import { signInWithEmailAndPassword } from "firebase/auth";
// Lib Imports
import { auth as authConfig } from "../firebase-config";
import { customInitApp } from "../firebase-admin";
// Needs to "init" on each call to the API
customInitApp();
// Login with Email/Password
async function handleEmailAndPassword(email, password) {
try {
var userCredential = await signInWithEmailAndPassword(authConfig,email,password);
if (userCredential.user.accessToken) {
var token = await auth().verifyIdToken(userCredential.user.accessToken);
if (token) {
var expiresIn = 20 * 60 * 1000; // 20 minutes
var sessionCookie = await auth().createSessionCookie(userCredential.user.accessToken, {expiresIn,});
var options = {
name: "session",
value: sessionCookie,
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
};
cookies().set(options);
return NextResponse.json({ options }, { status: 200 });
}
}
} catch (error) {
return NextResponse.json({ error: error.code }, { status: 401 });
}
}
// Handles POST requests (login requests)
export async function POST(req, res) {
try {
var { email, password } = await req?.json()
return await handleEmailAndPassword(email, password); // need session token
} catch (error) {
return NextResponse.json({ error: "Internal Server Error" },{ status: 500 });
}
}
// Handles GET requests (is session still valid requests)
export async function GET(req) {
var session = cookies().get("session")?.value || "";
//Validate if the cookie exist in the request
if (!session) {
return NextResponse.json({ isLogged: false }, { status: 401 });
} else {
// Validate session cookie
try {
var validation = await auth().verifySessionCookie(session, true);
return NextResponse.json({ isLogged: true, uid: validation.uid, email: validation.email }, { status: 200 });
} catch (error) {
return NextResponse.json({ isLogged: false}, { status: 401 });
}
}
}
@@ -0,0 +1,40 @@
import { NextResponse } from "next/server";
// Lib Imports
import { app } from "../firebase-config";
import { getDatabase, ref, set as firebaseSet } from "firebase/database";
async function onboard(firstName, lastName, req) {
var session = req.cookies.get("session");
//Call the authentication endpoint
var res = await fetch("http://localhost:3000/api/login", {headers: {Cookie: `session=${session?.value}`}})
// Login if unauthorized
if (res.status !== 200) {
return NextResponse.json({}, { status: 401 });
}
try {
var { uid, email } = await res.json()
var database = getDatabase(app)
await firebaseSet(ref(database, `users/${uid}`), {
firstName: firstName,
lastName: lastName,
email: email
});
return NextResponse.json({}, { status: 200 });
} catch(error) {
return NextResponse.json({ error: "Internal Server Error" },{ status: 500 });
}
}
// Handles POST requests (login requests)
export async function POST(req, res) {
try {
var { firstName, lastName } = await req?.json()
return await onboard(firstName, lastName, req);
} catch (error) {
console.log(error)
return NextResponse.json({ error: "Internal Server Error" },{ status: 500 });
}
}
@@ -0,0 +1,42 @@
// Import necessary functions
import { createUserWithEmailAndPassword } from "firebase/auth";
import { auth } from "../firebase-config";
import { NextResponse } from "next/server";
// Function to register a new user using Firebase Authentication
export async function registerUser(email, password) {
try {
var userCredential = await createUserWithEmailAndPassword(auth,email,password);
// You can perform additional actions after successful registration, if needed.
return { success: true, userCredential };
} catch (error) {
return { success: false, error: error.message };
}
}
// POST request handler
export async function POST(req, res) {
try {
// Extract email and password from the request body
var { email, password } = await req?.json();
// Check if email and password are provided
if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required." },
{ status: 400 }
);
}
// Register the user
try {
var userCredential = await createUserWithEmailAndPassword(auth,email,password);
return NextResponse.json({message: "Registration successful.",user: userCredential.user,});
} catch {
return NextResponse.json({ error: registrationResult.error },{ status: 500 });
}
} catch (error) {
// Handle unexpected errors
return NextResponse.json({ error: "Internal Server Error" },{ status: 500 });
}
}
@@ -0,0 +1,17 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function GET(req) {
cookies().set({
name: "session",
value: "",
maxAge: -1,
});
cookies().set({
name: "firstName",
value: "",
maxAge: -1,
});
return NextResponse.json({}, { status: 200 });
}
+15
View File
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
export async function GET(req) {
const session = cookies().get("session");
// Login if not logged in
if (session) {
return NextResponse.json({
firstName: cookies().get("firstName")?.value,
lastName: cookies().get("lastName")?.value,
uid: cookies().get("uid")?.value,
})
}
return NextResponse.json({}, { status: 500 });
}
+26
View File
@@ -0,0 +1,26 @@
import { Inter } from "next/font/google";
import "../globals.css";
import { Header, Sidebar } from "./shared"
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Home",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<div className="grid grid-cols-4 auto-cols-max overflow-hidden">
<div className="col-span-3 h-page">
<Header/>
{children}
</div>
<Sidebar/>
</div>
</body>
</html>
);
}
+72
View File
@@ -0,0 +1,72 @@
"use client"
import { useState, useEffect } from 'react'
import {Map} from "pigeon-maps"
function WelcomeMessage() {
//TODO: REALLY GROSS WAY TO GET COOKIES, NEED NEW WAY TO STORE USER DATA WITHOUT API CALLS. THIS PAGE HAS TO BE CLIENT SIDE DUE TO MAPS / GEOLOCATION
const [data, setData] = useState(null)
const [isLoading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then((data) => {
setData(data)
setLoading(false)
})
}, [])
if (isLoading) return <div></div>
if (!data) return <div></div>
return (
<div className="bg-white rounded-lg m-2 mt-4 text-left p-2 pl-5">
<div>
Welcome, {data.firstName} {data.lastName}
</div>
<div>
Lets see what&apos;s happening in your area.
</div>
</div>
)
}
function Geo() {
const [isLoading, setLoading] = useState(true)
const [data, setData] = useState();
useEffect(() => {
if('geolocation' in navigator) {
// Retrieve latitude & longitude coordinates from `navigator.geolocation` Web API
navigator.geolocation.getCurrentPosition(({ coords }) => {
const { latitude, longitude } = coords;
console.log(latitude, longitude)
setData(coords)
setLoading(false)
})
}
}, []);
if (!isLoading) {
return (
<Map className="rounded-lg" defaultCenter={[data.latitude, data.longitude]} defaultZoom={14}/>
)
} else {
return (
<div>Loading...</div>
)
}
}
function Home() {
return (
<div className="h-[calc(100%-75px)]">
<WelcomeMessage/>
<div className='h-[calc(100%-110px)] m-5 rounded-lg'>
<Geo/>
</div>
</div>
)
}
export default Home;
+18
View File
@@ -0,0 +1,18 @@
export function Header() {
return (
<div className="m-2 rounded-lg h-[60px] bg-white shadow-2xl">
<a href="/"><img src="/logos/logo_transparent_inverse.png" className="h-[60px]"></img></a>
</div>
)
}
export function Sidebar() {
return (
<div className="h-dvh">
<div className="bg-white shadow-2xl rounded-lg m-2 h-[98%]">
Sidebar
</div>
</div>
)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+6 -2
View File
@@ -2,17 +2,20 @@
@tailwind components;
@tailwind utilities;
main {
body {
background-color: aliceblue;
text-align: center;
text-wrap:pretty;
}
button {
background-color: rgb(205, 205, 205);
background: #dee0e0;
border-color: black;
border: 5px;
border-radius: 5px;
padding: 5px;
margin: 5px;
filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
}
button:hover {
@@ -23,5 +26,6 @@ input {
border: 1px solid black;
border-radius: 4px;
padding: 10px 10px;
margin: 5px;
}
+4 -2
View File
@@ -5,13 +5,15 @@ const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps",
description: "ChatMaps: Social Media for College Students",
description: "ChatMaps: Social Media for College Students"
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
{children}
</body>
</html>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Login",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
+43
View File
@@ -0,0 +1,43 @@
"use client";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import "../globals.css"
function Login() {
var router = useRouter();
var { register, handleSubmit } = useForm();
async function Login(data) {
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(data ? data : {}),
});
if (res.ok) {
router.push("/app");
}
}
return (
<div>
<div className="grid h-screen place-items-center">
<div>
<img src="logos/logo_transparent_inverse.png"/>
<span className="text-[36px]">
Chat with friends!
</span>
<div className="m-5">
<h3 className="text-[24px] mt-[50px]">Login</h3>
<form action="#" onSubmit={handleSubmit(Login)}>
<input type="email" {...register("email")} placeholder="Enter Email Address"/><br/>
<input type="password" {...register("password")} placeholder="Enter Password"/><br/>
<button type="submit" className="bg-[#dee0e0] m-5">Login</button><br/>
Don&apos;t have an account? <a href="/register">Sign Up</a>
</form>
</div>
</div>
</div>
</div>
)
}
export default Login;
@@ -0,0 +1,19 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Onboarding",
description: "ChatMaps: Social Media for College Students"
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
+44
View File
@@ -0,0 +1,44 @@
"use client";
import "../globals.css"
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
function Onboarding() {
var router = useRouter();
var { register, handleSubmit } = useForm();
async function Onboard(data) {
const res = await fetch("/api/onboard", {
method: "POST",
body: JSON.stringify(data ? data : {}),
});
if (res.ok) {
router.push("/app");
} else {
router.push("/login");
}
}
return (
<div>
<div className="grid h-screen place-items-center">
<div>
<img src="logos/logo_transparent_inverse.png"/>
<span className="text-[36px]">
Chat with friends!
</span>
<div className="m-5">
Welcome to ChatMaps! We are excited to have you join our community!<br/>First we just need a little bit of information from you to get started.
</div>
<form action="#" onSubmit={handleSubmit(Onboard)}>
<input type="text" {...register("firstName")} placeholder="First Name"/><br/>
<input type="text" {...register("lastName")} placeholder="Last Name"/><br/>
<button type="submit" className="bg-[#dee0e0] m-5">Save</button>
</form>
</div>
</div>
</div>
)
}
export default Onboarding;
+23 -26
View File
@@ -1,27 +1,24 @@
export default function Home() {
return (
<main className="flex min-h-screen flex-col justify-between p-24">
<h1>Welcome to ChatMaps.</h1>
<div id="room">
<label>Room </label>
<select>
<option>Room 1</option>
<option>Room 2</option>
<option>Room 3</option>
</select>
<button>Join Room</button>
</div>
<div id="message">
<label>Enter a message</label>
<input />
<button>Send</button>
</div>
</main>
);
function Home() {
return (
<div>
<div className="absolute right-[6%] top-[4%]">
<button>Download</button>
</div>
<div className="grid h-screen place-items-center">
<div>
<img src="logos/logo_transparent_inverse.png"/>
<span className="text-[36px]">
Chat with friends!
</span>
<div className="m-5">
<a href="/login"><button>Login</button></a>
<a href="/register"><button>Signup</button></a>
</div>
</div>
</div>
</div>
)
}
export default Home;
+19
View File
@@ -0,0 +1,19 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: Register",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
}
+43
View File
@@ -0,0 +1,43 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import "../globals.css"
function Register() {
var {register, handleSubmit } = useForm()
var router = useRouter();
async function RegisterWithEmail(data) {
const res = await fetch("/api/register", {
method: "POST",
body: JSON.stringify(data ? data : {}),
});
if (res.ok) {
router.push("/login");
}
}
return (
<div>
<div className="grid h-screen place-items-center">
<div>
<img src="logos/logo_transparent_inverse.png"/>
<span className="text-[36px]">
Chat with friends!
</span>
<div className="m-5">
<h3 className="text-[24px] mt-[50px]">Register</h3>
<form action="#" onSubmit={handleSubmit(RegisterWithEmail)}>
<input type="email" {...register("email")} placeholder="Enter Email Address"/><br/>
<input type="password" {...register("password")} placeholder="Enter Password"/><br/>
<button type="submit" className="bg-[#dee0e0] m-5">Register</button><br/>
Have an account? <a href="/login">Log In</a>
</form>
</div>
</div>
</div>
</div>
)
}
export default Register;
@@ -0,0 +1,26 @@
import { Inter } from "next/font/google";
import "../../globals.css";
import { Header, Sidebar } from "./shared"
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<div className="grid grid-cols-4 auto-cols-max">
<div className="col-span-3 h-dvh">
<Header/>
{children}
</div>
<Sidebar/>
</div>
</body>
</html>
);
}
+28
View File
@@ -0,0 +1,28 @@
function MessagesDisplayBox() {
return (
<div className="h-[98%]">
Messages Stream in Here
</div>
)
}
function MessageSendBox() {
return (
<div className="bg-white rounded-lg shadow-2xl w-[98%] m-2">
Message Sender
</div>
)
}
function Home() {
return (
<div className="">
<MessagesDisplayBox/>
<MessageSendBox/>
</div>
)
}
export default Home;
@@ -0,0 +1,18 @@
export function Header() {
return (
<div className="m-2 rounded-lg h-[60px] bg-white shadow-2xl">
<img src="/logos/logo_transparent_inverse.png" className="h-[60px]"></img>
</div>
)
}
export function Sidebar() {
return (
<div className="h-dvh">
<div className="bg-white shadow-2xl rounded-lg m-2 h-[98%]">
Sidebar
</div>
</div>
)
}
+49
View File
@@ -0,0 +1,49 @@
// src/middleware.js
import { NextResponse } from "next/server";
import { app } from "./app/api/firebase-config";
import { getDatabase, ref, get as firebaseGet } from "firebase/database";
export async function middleware(req, res) {
const session = req.cookies.get("session");
// Login if not logged in
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
//Call the authentication endpoint
const responseAPI = await fetch("http://localhost:3000/api/login", {
headers: {
Cookie: `session=${session?.value}`,
},
});
// Login if unauthorized
if (responseAPI.status !== 200) {
return NextResponse.redirect(new URL("/login", req.url));
}
// If new user, redirect to onboarding
var { uid } = await responseAPI.json()
var firstName = await req.cookies.get("firstName")?.value;
if (firstName) {
return NextResponse.next();
} else {
var database = getDatabase(app)
var user = await firebaseGet(ref(database, `users/${uid}`));
if (!user.exists()) {
return NextResponse.redirect(new URL("/onboarding", req.url));
} else {
var returnedResponse = NextResponse.next();
returnedResponse.cookies.set("firstName",user.val()?.firstName)
returnedResponse.cookies.set("lastName",user.val()?.lastName)
returnedResponse.cookies.set("uid",uid)
return returnedResponse
}
}
}
//Protected routes
export const config = {
matcher: ['/((?!login|register|onboarding|api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|logo|$).*)',],
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
};
-9
View File
@@ -5,14 +5,5 @@ module.exports = {
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};