├── .gitignore ├── LICENSE ├── README.md ├── jsvectormap.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── protein bind.drawio ├── protein bind.drawio.zip ├── public ├── drug_research.jpg └── images │ ├── favicon.ico │ ├── logo │ ├── dna.svg │ └── logo.svg │ └── user │ └── user-01.png ├── src ├── app │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── send-reset-password-email │ │ │ └── route.ts │ │ └── send-verification-email │ │ │ └── route.ts │ ├── auth-page │ │ ├── signin │ │ │ └── page.tsx │ │ └── signup │ │ │ └── page.tsx │ ├── context │ │ └── UserContext.tsx │ ├── favicon.ico │ ├── forget-password │ │ └── page.tsx │ ├── layout.tsx │ ├── message │ │ ├── chat-box.jsx │ │ ├── chat.jsx │ │ └── page.jsx │ ├── model │ │ └── page.tsx │ ├── molecule-bank │ │ └── page.tsx │ ├── page.tsx │ ├── profile │ │ └── page.tsx │ ├── research │ │ └── page.tsx │ ├── reset-password │ │ └── page.tsx │ ├── settings │ │ └── page.tsx │ ├── ui │ │ └── modals │ │ │ └── page.tsx │ └── verify-email │ │ └── page.tsx ├── components │ ├── ClickOutside.tsx │ ├── ComponentHeader │ │ └── ComponentHeader.tsx │ ├── EmailTemplates │ │ ├── reset-email.tsx │ │ └── verify-email.tsx │ ├── Header │ │ ├── DarkModeSwitcher.tsx │ │ ├── DropdownMessage.tsx │ │ ├── DropdownUser.tsx │ │ └── index.tsx │ ├── Layouts │ │ └── DefaultLayout.tsx │ ├── MoleculeBank │ │ └── MoleculeBankTable.tsx │ ├── MoleculeStructure │ │ └── index.jsx │ ├── Sidebar │ │ ├── SidebarDropdown.tsx │ │ ├── SidebarItem.tsx │ │ └── index.tsx │ ├── common │ │ └── Loader │ │ │ └── index.tsx │ └── dashboard │ │ ├── components │ │ ├── CTACard.tsx │ │ ├── DashboardCardChat.tsx │ │ └── DashboardCardMap.tsx │ │ └── index.tsx ├── css │ └── style.css ├── hooks │ ├── useColorMode.tsx │ └── useLocalStorage.tsx ├── js │ ├── us-aea-en.js │ └── world.js ├── lib │ ├── actions │ │ ├── email.actions.ts │ │ ├── group.actions.ts │ │ ├── molecule-generation.action.ts │ │ └── user.actions.ts │ ├── database │ │ ├── models │ │ │ ├── group.model.ts │ │ │ ├── molecule-generation.model.ts │ │ │ └── user.model.ts │ │ └── mongoose.ts │ └── utils.ts └── types │ └── index.d.ts ├── tailadmin-nextjs.jpg ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | .yarn/install-state.gz 6 | descriptions.txt 7 | # testing 8 | /coverage 9 | oldpage 10 | # next.js 11 | /.next/ 12 | /out/ 13 | .env 14 | /.env 15 | # production 16 | /build 17 | outline.md 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TailAdmin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | ProteinBind Project Banner 5 | 6 | 7 |
8 | 9 |
10 | typescript 11 | nextdotjs 12 | tailwindcss 13 | nvidia-neMo 14 |
15 | 16 |

ProteinBind

17 | 18 |
19 | Build this project step by step with our detailed tutorial on Your YouTube Channel. Join the community! 20 |
21 |
22 | 23 | ## 📋 Table of Contents 24 | 25 | 1. 🤖 [Introduction](#introduction) 26 | 2. ⚙️ [Tech Stack](#tech-stack) 27 | 3. 🔋 [Features](#features) 28 | 4. 🤸 [Quick Start](#quick-start) 29 | 5. 🧬 [Protein Data Processing](#protein-data) 30 | 6. 🚀 [More](#more) 31 | 32 | ## 🚨 Tutorial 33 | 34 | This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, Code with Albert. 35 | 36 | If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! 37 | 38 | ## 🤖 Introduction 39 | 40 | **ProteinBind** is a drug discovery and protein-binding prediction tool built with the latest in machine learning and natural language processing (NLP) technology. Powered by NVIDIA NIM and protein structure prediction models, this project enables users to simulate molecular interactions and predict protein structures. 41 | 42 | The platform is designed to help researchers accelerate drug discovery by leveraging cutting-edge AI models for protein folding, docking, and molecular dynamics. 43 | 44 | If you're getting started and need assistance or face any bugs, join our active Discord community. It's a place where people help each other out. 45 | 46 | 47 | 48 | ## ⚙️ Tech Stack 49 | 50 | - **Next.js** 51 | - **TypeScript** 52 | - **NVIDIA** (for protein structure prediction) 53 | - **Tailwind CSS** 54 | - **React Chart.js** (for visualizing protein data) 55 | 56 | ## 🔋 Features 57 | 58 | 👉 **Protein Structure Prediction**: Predicts 2D protein structures using NVIDIA models. 59 | 60 | 👉 **Collaborative Research**: Researches can create groups and colloborate with other research online 61 | 62 | 👉 **Responsive Design**: Ensures seamless experience across all devices, from desktops to mobile. 63 | 64 | ## 🤸 Quick Start 65 | 66 | Follow these steps to set up the project locally on your machine. 67 | 68 | ### **Prerequisites** 69 | 70 | Make sure you have the following installed on your machine: 71 | 72 | - [Git](https://git-scm.com/) 73 | - [Node.js](https://nodejs.org/en) 74 | - [npm](https://www.npmjs.com/) (Node Package Manager) 75 | 76 | ### **Cloning the Repository** 77 | 78 | ```bash 79 | git clone https://github.com/mendsalbert/ProteinBind.git 80 | cd proteinbind 81 | ``` 82 | 83 | ### **Installation** 84 | 85 | Install the project dependencies using npm: 86 | 87 | ```bash 88 | npm install 89 | ``` 90 | 91 | ### **Set Up Environment Variables** 92 | 93 | Create a new file named `.env` in the root of your project and add the following content: 94 | 95 | ```env 96 | NEXT_PUBLIC_NVIDIA_API_KEY=your-nvidia-api-key 97 | 98 | ABLY_API_KEY='your-ably-api-key' 99 | 100 | MONGODB_URL='your-mongodb-url' 101 | 102 | NEXT_PUBLIC_API_BASE_URL=http://localhost:3000 103 | 104 | RESEND_KEY='your-resend-api-key' 105 | ``` 106 | 107 | ### **Running the Project** 108 | 109 | ```bash 110 | npm run dev 111 | ``` 112 | 113 | Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. 114 | 115 | ## 🧬 Protein Data Processing 116 | 117 | This section covers the protein data processing pipeline, including loading protein structure files (e.g., PDB format), performing molecular docking simulations, and visualizing the results. 118 | 119 | ### **Protein Structure Input** 120 | 121 | Users can upload PDB files for protein structures, which will then be processed by NVIDIA NeMo's protein-folding models. 122 | 123 | ### **Docking Simulation** 124 | 125 | Using molecular docking algorithms, the system predicts how small molecules (such as drug candidates) bind to protein targets. 126 | 127 | ## 🚀 More 128 | 129 | Stay tuned for more updates and features! Join our community, contribute to the repository, and follow along with our detailed tutorials. 130 | 131 | ## 📞 **Contact & Community** 132 | 133 | If you have any questions or need support, feel free to join our Discord server or contact us through GitHub Discussions. 134 | 135 | Join Discord | GitHub Discussions 136 | -------------------------------------------------------------------------------- /jsvectormap.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jsvectormap' { 2 | const jsVectorMap: any; 3 | export default jsVectorMap; 4 | } 5 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require("copy-webpack-plugin"); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | webpack(config, { isServer }) { 6 | config.plugins.push( 7 | new CopyPlugin({ 8 | patterns: [ 9 | { 10 | from: "node_modules/@rdkit/rdkit/dist/RDKit_minimal.wasm", 11 | to: "static/chunks", 12 | }, 13 | ], 14 | }), 15 | ); 16 | 17 | if (!isServer) { 18 | config.resolve.fallback = { 19 | fs: false, 20 | }; 21 | } 22 | 23 | return config; 24 | }, 25 | }; 26 | 27 | module.exports = nextConfig; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "free-nextjs-admin-dashboard", 3 | "version": "1.3.4", 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 | "@next-auth/prisma-adapter": "^1.0.7", 13 | "@rdkit/rdkit": "^2023.9.2-1.0.0", 14 | "@xmtp/content-type-reaction": "^1.1.9", 15 | "@xmtp/content-type-remote-attachment": "^1.1.9", 16 | "@xmtp/content-type-reply": "^1.1.11", 17 | "@xmtp/message-kit": "^0.0.9", 18 | "@xmtp/react-sdk": "^8.0.1", 19 | "ably": "^2.3.1", 20 | "apexcharts": "^3.45.2", 21 | "bcrypt": "^5.1.1", 22 | "copy-webpack-plugin": "^12.0.2", 23 | "dotenv": "^16.4.5", 24 | "ethers": "^5.7.0", 25 | "flatpickr": "^4.6.13", 26 | "fs": "^0.0.1-security", 27 | "hex-rgb": "^5.0.0", 28 | "install": "^0.13.0", 29 | "jsvectormap": "^1.6.0", 30 | "lodash": "^4.17.21", 31 | "lucide-react": "^0.427.0", 32 | "mongodb": "^6.3.0", 33 | "mongoose": "^8.1.1", 34 | "next": "^14.2.4", 35 | "next-auth": "^4.24.7", 36 | "prop-types": "^15.8.1", 37 | "react": "^18.3.1", 38 | "react-apexcharts": "^1.4.1", 39 | "react-dom": "^18", 40 | "resend": "^3.5.0" 41 | }, 42 | "devDependencies": { 43 | "@types/bcrypt": "^5.0.2", 44 | "@types/node": "^20", 45 | "@types/react": "^18", 46 | "@types/react-dom": "^18", 47 | "autoprefixer": "^10.0.1", 48 | "daisyui": "^4.12.10", 49 | "eslint": "^8", 50 | "eslint-config-next": "14.1.0", 51 | "node-loader": "^2.0.0", 52 | "postcss": "^8", 53 | "prettier": "^3.2.5", 54 | "prettier-plugin-tailwindcss": "^0.5.11", 55 | "prisma": "^5.18.0", 56 | "tailwindcss": "^3.4.1", 57 | "typescript": "^5" 58 | }, 59 | "browser": { 60 | "fs": false, 61 | "path": false, 62 | "os": false 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier-plugin-tailwindcss'], 3 | } -------------------------------------------------------------------------------- /protein bind.drawio.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendsalbert/ProteinBind/654560d8d17542170ae40170254650993859736e/protein bind.drawio.zip -------------------------------------------------------------------------------- /public/drug_research.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendsalbert/ProteinBind/654560d8d17542170ae40170254650993859736e/public/drug_research.jpg -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendsalbert/ProteinBind/654560d8d17542170ae40170254650993859736e/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/logo/dna.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/logo/logo.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /public/images/user/user-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendsalbert/ProteinBind/654560d8d17542170ae40170254650993859736e/public/images/user/user-01.png -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import CredentialsProvider from "next-auth/providers/credentials"; 3 | import { loginUser } from "@/lib/actions/user.actions"; 4 | 5 | export const handler = NextAuth({ 6 | providers: [ 7 | CredentialsProvider({ 8 | name: "Credentials", 9 | credentials: { 10 | email: { label: "Email", type: "email" }, 11 | password: { label: "Password", type: "password" }, 12 | } as any, 13 | async authorize(credentials) { 14 | if (credentials?.email && credentials?.password) { 15 | const user = await loginUser(credentials.email, credentials.password); 16 | if (user) { 17 | return user; 18 | } else { 19 | return null; 20 | } 21 | } 22 | 23 | return null; 24 | }, 25 | }), 26 | ], 27 | session: { 28 | strategy: "jwt", 29 | maxAge: 24 * 60 * 60, 30 | }, 31 | callbacks: { 32 | async jwt({ token, user }) { 33 | if (user) { 34 | token.id = user.id; 35 | } 36 | return token; 37 | }, 38 | async session({ session, token }: any) { 39 | if (token) { 40 | session.id = token.id; 41 | } 42 | return session; 43 | }, 44 | }, 45 | secret: process.env.NEXTAUTH_SECRET, 46 | }); 47 | 48 | export { handler as GET, handler as POST }; 49 | -------------------------------------------------------------------------------- /src/app/api/send-reset-password-email/route.ts: -------------------------------------------------------------------------------- 1 | import { ResetPasswordTemplate } from "@/components/EmailTemplates/reset-email"; 2 | import { Resend } from "resend"; 3 | const resend = new Resend(process.env.RESEND_KEY); 4 | 5 | export async function POST(request: Request) { 6 | const { firstName, email, resetUrl } = await request.json(); 7 | 8 | try { 9 | const { data, error } = await resend.emails.send({ 10 | from: "ProteinBind ", 11 | to: [email], 12 | subject: "Reset your password", 13 | react: ResetPasswordTemplate({ firstName, resetUrl }), 14 | }); 15 | 16 | if (error) { 17 | return new Response(JSON.stringify({ error }), { status: 500 }); 18 | } 19 | 20 | return new Response(JSON.stringify(data), { status: 200 }); 21 | } catch (error: any) { 22 | return new Response(JSON.stringify({ error: error.message }), { 23 | status: 500, 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/api/send-verification-email/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/send-verification-email/route.ts 2 | 3 | import { VerifyEmailTemplate } from "@/components/EmailTemplates/verify-email"; 4 | import { Resend } from "resend"; 5 | const resend = new Resend(process.env.RESEND_KEY); 6 | 7 | export async function POST(request: Request) { 8 | const { firstName, email, verificationUrl } = await request.json(); 9 | 10 | try { 11 | const { data, error } = await resend.emails.send({ 12 | from: "ProteinBind ", 13 | to: [email], 14 | subject: "Verify your email", 15 | react: VerifyEmailTemplate({ firstName, verificationUrl }), 16 | }); 17 | 18 | if (error) { 19 | console.log(error); 20 | return new Response(JSON.stringify({ error }), { status: 500 }); 21 | } 22 | 23 | return new Response(JSON.stringify(data), { status: 200 }); 24 | } catch (error: any) { 25 | console.log(error); 26 | 27 | return new Response(JSON.stringify({ error: error.message }), { 28 | status: 500, 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/auth-page/signin/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import Link from "next/link"; 4 | import { LoaderCircle, LockIcon, MailIcon } from "lucide-react"; 5 | import DefaultLayout from "@/components/Layouts/DefaultLayout"; 6 | import ComponentHeader from "@/components/ComponentHeader/ComponentHeader"; 7 | import { useRouter } from "next/navigation"; 8 | import { signIn } from "next-auth/react"; 9 | 10 | const SignIn: React.FC = () => { 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [error, setError] = useState(null); 14 | const [isLoading, setIsLoading] = useState(false); 15 | const router = useRouter(); 16 | 17 | const handleSubmit = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | setError(null); 20 | setIsLoading(true); 21 | 22 | // Basic form validation 23 | if (!email || !password) { 24 | setError("Please fill in all fields."); 25 | setIsLoading(false); 26 | return; 27 | } 28 | 29 | try { 30 | // Use NextAuth's signIn function 31 | const result = await signIn("credentials", { 32 | redirect: false, // Prevent auto-redirect 33 | email, 34 | password, 35 | }); 36 | 37 | if (result?.error) { 38 | setError(result.error); // Handle login error 39 | } else { 40 | // Navigate to the dashboard or another page on successful login 41 | router.push("/"); 42 | } 43 | } catch (err: any) { 44 | // Handle unexpected error 45 | setError("Something went wrong. Please try again."); 46 | console.error(err); 47 | } finally { 48 | setIsLoading(false); 49 | } 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 56 |
57 |
58 |
59 |
60 | Start for free 61 |

62 | Sign In to ProteinBind 63 |

64 | 65 | {error &&
{error}
} 66 | 67 |
68 |
69 | 72 |
73 | setEmail(e.target.value)} 77 | placeholder="Enter your email" 78 | className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-black outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary" 79 | required 80 | disabled={isLoading} 81 | /> 82 | 83 | 84 | 85 |
86 |
87 | 88 |
89 | 92 |
93 | setPassword(e.target.value)} 97 | placeholder="6+ Characters, 1 Capital letter" 98 | className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-black outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary" 99 | required 100 | disabled={isLoading} 101 | /> 102 | 103 | 104 | 105 |
106 |
107 | 108 |
109 | 123 |
124 | 125 |
126 |

127 | Don’t have an account?{" "} 128 | 129 | Sign Up 130 | 131 |

132 |

133 | Forgot Password?{" "} 134 | 135 | Reset 136 | 137 |

138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | ); 146 | }; 147 | 148 | export default SignIn; 149 | -------------------------------------------------------------------------------- /src/app/auth-page/signup/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useCallback } from "react"; 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import ComponentHeader from "@/components/ComponentHeader/ComponentHeader"; 6 | import DefaultLayout from "@/components/Layouts/DefaultLayout"; 7 | import { createUser } from "@/lib/actions/user.actions"; 8 | import { 9 | CameraIcon, 10 | LoaderCircle, 11 | LockIcon, 12 | MailIcon, 13 | UserIcon, 14 | } from "lucide-react"; 15 | 16 | const SignUp: React.FC = () => { 17 | const [user, setUser] = useState({ 18 | email: "", 19 | firstName: "", 20 | lastName: "", 21 | password: "", 22 | confirmPassword: "", 23 | photo: "", 24 | userBio: "", 25 | }); 26 | 27 | const [imageFile, setImageFile] = useState(null); 28 | const [errors, setErrors] = useState(null); 29 | const [isLoading, setIsLoading] = useState(false); 30 | 31 | const handleInputChange = ( 32 | e: React.ChangeEvent, 33 | ) => { 34 | const { name, value } = e.target; 35 | setUser((prev) => ({ 36 | ...prev, 37 | [name]: value, // Ensure the value is treated as a string 38 | })); 39 | }; 40 | 41 | const handleFileChange = (e: React.ChangeEvent) => { 42 | const file = e.target.files ? e.target.files[0] : null; 43 | setImageFile(file); 44 | }; 45 | 46 | const convertImageToBase64 = async (file: File): Promise => { 47 | return new Promise((resolve, reject) => { 48 | const reader = new FileReader(); 49 | reader.readAsDataURL(file); 50 | reader.onloadend = () => { 51 | resolve(reader.result as string); 52 | }; 53 | reader.onerror = (error) => { 54 | reject(error); 55 | }; 56 | }); 57 | }; 58 | 59 | // Validate form fields 60 | const validateForm = useCallback(() => { 61 | if ( 62 | !user.email || 63 | !user.firstName || 64 | !user.lastName || 65 | !user.password || 66 | !user.confirmPassword 67 | ) { 68 | return "Please fill in all the fields."; 69 | } 70 | if (user.password !== user.confirmPassword) { 71 | return "Passwords do not match."; 72 | } 73 | if (!imageFile) { 74 | return "Please upload a profile picture."; 75 | } 76 | return null; 77 | }, [user, imageFile]); 78 | 79 | let isSubmitting = false; 80 | const handleSubmit = async (e: React.FormEvent) => { 81 | e.preventDefault(); 82 | 83 | if (isSubmitting) return; 84 | isSubmitting = true; 85 | 86 | setIsLoading(true); 87 | 88 | const formError = validateForm(); 89 | if (formError) { 90 | setErrors(formError); 91 | setIsLoading(false); 92 | isSubmitting = false; 93 | return; 94 | } 95 | 96 | setErrors(null); 97 | 98 | try { 99 | let base64Image = ""; 100 | if (imageFile) { 101 | base64Image = await convertImageToBase64(imageFile); 102 | } 103 | 104 | const newUser = { 105 | ...user, 106 | photo: base64Image, 107 | }; 108 | 109 | console.log(newUser); 110 | const createdUser = await createUser(newUser); 111 | console.log(createdUser); 112 | 113 | setUser({ 114 | email: "", 115 | firstName: "", 116 | lastName: "", 117 | password: "", 118 | confirmPassword: "", 119 | photo: "", 120 | userBio: "", 121 | }); 122 | setImageFile(null); 123 | setIsLoading(false); 124 | } catch (error) { 125 | console.error("Error registering user:", error); 126 | setErrors("Registration failed."); 127 | } finally { 128 | isSubmitting = false; 129 | setIsLoading(false); 130 | } 131 | }; 132 | 133 | return ( 134 | 135 | 136 | 137 |
138 |
139 |
140 |
141 | 142 |
143 |
144 | Logo 151 |
152 |

153 | ProteinBind 154 |

155 |
156 | 157 | 158 | 159 | 166 | 170 | 174 | 178 | 182 | 186 | 190 | 194 | 198 | 202 | 206 | 210 | 214 | 218 | 222 | 226 | 230 | 234 | 238 | 242 | 246 | 250 | 254 | 258 | 262 | 266 | 270 | 274 | 278 | 279 | 280 |
281 |
282 | 283 |
287 |
288 | Start for free 289 |

290 | Sign Up to ProteinBind 291 |

292 | 293 |
294 |
295 | 298 |
299 | 307 | 308 | 309 | 310 | 311 |
312 |
313 | 314 |
315 | 318 |
319 | 327 | 328 | 329 | 330 | 331 |
332 |
333 |
334 | 337 |
338 | 346 | 347 | 348 | 349 | 350 |
351 |
352 | 353 |
354 | 357 |
358 | 366 | 367 | 368 | 369 | 370 |
371 |
372 | 373 |
374 | 377 |
378 | 386 | 387 | 388 | 389 |
390 |
391 | 392 |
393 | 396 |
397 | 221 |
222 |
223 | 224 |
225 | 231 | 238 |
239 | 240 |
241 |
242 |
243 | 244 | {/* Image Upload Form */} 245 |
246 |
247 |
248 |

249 | Your Photo 250 |

251 |
252 |
253 |
254 |
255 |
256 | User 263 |
264 |
265 | 266 | Edit your photo 267 | 268 | 269 | 276 | 285 | 286 |
287 |
288 | 289 |
293 | 300 |
301 | 302 | 303 | 304 |

305 | Click to upload or 306 | drag and drop 307 |

308 |

SVG, PNG, JPG or GIF

309 |

(max, 800 X 800px)

310 |
311 |
312 | 313 |
314 | 320 | 327 |
328 |
329 |
330 |
331 |
332 |
333 | 334 |
335 | ); 336 | }; 337 | 338 | export default Settings; 339 | -------------------------------------------------------------------------------- /src/app/ui/modals/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Modal: React.FC = ({ id, title, content, onCloseText }) => { 4 | return ( 5 | 6 |
7 |

{title}

8 |
{content}
9 |
10 |
11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default Modal; 18 | -------------------------------------------------------------------------------- /src/app/verify-email/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect, useState } from "react"; 4 | import { useRouter, useSearchParams } from "next/navigation"; 5 | import { verifyEmail } from "@/lib/actions/user.actions"; 6 | import DefaultLayout from "@/components/Layouts/DefaultLayout"; 7 | import { CircleCheckBig } from "lucide-react"; 8 | 9 | const VerifyEmailPage: React.FC = () => { 10 | const [status, setStatus] = useState<"loading" | "success" | "error">( 11 | "loading", 12 | ); 13 | const searchParams = useSearchParams(); 14 | const token = searchParams.get("token"); 15 | const router = useRouter(); 16 | 17 | useEffect(() => { 18 | const verifyUserEmail = async () => { 19 | if (token) { 20 | try { 21 | await verifyEmail(token); 22 | setStatus("success"); 23 | } catch (error) { 24 | console.error("Error verifying email:", error); 25 | setStatus("error"); 26 | } 27 | } else { 28 | setStatus("error"); 29 | } 30 | }; 31 | 32 | verifyUserEmail(); 33 | }, [token]); 34 | 35 | return ( 36 | 37 |
38 | 39 | 40 | 41 | {status === "loading" &&

Verifying your email, please wait...

} 42 | {status === "success" && ( 43 |

Your email has been successfully verified!

44 | )} 45 | {status === "error" && ( 46 |

There was an error verifying your email. Please try again.

47 | )} 48 | {status === "success" && ( 49 | 55 | )} 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default VerifyEmailPage; 62 | -------------------------------------------------------------------------------- /src/components/ClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | 3 | interface Props { 4 | children: React.ReactNode; 5 | exceptionRef?: React.RefObject; 6 | onClick: () => void; 7 | className?: string; 8 | } 9 | 10 | const ClickOutside: React.FC = ({ 11 | children, 12 | exceptionRef, 13 | onClick, 14 | className, 15 | }) => { 16 | const wrapperRef = useRef(null); 17 | 18 | useEffect(() => { 19 | const handleClickListener = (event: MouseEvent) => { 20 | let clickedInside: null | boolean = false; 21 | if (exceptionRef) { 22 | clickedInside = 23 | (wrapperRef.current && 24 | wrapperRef.current.contains(event.target as Node)) || 25 | (exceptionRef.current && exceptionRef.current === event.target) || 26 | (exceptionRef.current && 27 | exceptionRef.current.contains(event.target as Node)); 28 | } else { 29 | clickedInside = 30 | wrapperRef.current && 31 | wrapperRef.current.contains(event.target as Node); 32 | } 33 | 34 | if (!clickedInside) onClick(); 35 | }; 36 | 37 | document.addEventListener("mousedown", handleClickListener); 38 | 39 | return () => { 40 | document.removeEventListener("mousedown", handleClickListener); 41 | }; 42 | }, [exceptionRef, onClick]); 43 | 44 | return ( 45 |
46 | {children} 47 |
48 | ); 49 | }; 50 | 51 | export default ClickOutside; 52 | -------------------------------------------------------------------------------- /src/components/ComponentHeader/ComponentHeader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Modal from "@/app/ui/modals/page"; 3 | import React from "react"; 4 | 5 | interface ComponentHeaderProps { 6 | pageName: string; 7 | containActionButton?: boolean; 8 | } 9 | 10 | const ComponentHeader: React.FC = ({ 11 | pageName, 12 | containActionButton, 13 | }) => { 14 | const openModal = (modalId: string) => { 15 | const modal = document.getElementById(modalId) as HTMLDialogElement; 16 | if (modal) { 17 | modal.showModal(); 18 | } 19 | }; 20 | 21 | return ( 22 |
23 |

24 | {pageName} 25 |

26 | 27 | {containActionButton && ( 28 | 38 | )} 39 | 40 | 45 |
46 |
47 |
48 |
49 | 52 | 57 |
58 | 59 |
60 | 63 | 68 |
69 |
70 | 71 |
72 | 75 | 80 |
81 | 82 |
83 | 86 | 91 |
92 | 93 | 96 |
97 |
98 | 99 | } 100 | onCloseText="Close" 101 | /> 102 |
103 | ); 104 | }; 105 | 106 | export default ComponentHeader; 107 | -------------------------------------------------------------------------------- /src/components/EmailTemplates/reset-email.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface ResetPasswordTemplateProps { 4 | firstName: string; 5 | resetUrl: string; 6 | } 7 | 8 | export const ResetPasswordTemplate: React.FC< 9 | Readonly 10 | > = ({ firstName, resetUrl }) => ( 11 |
12 |

Hello, {firstName}!

13 |

14 | It looks like you requested a password reset. Click the link below to 15 | reset your password: 16 |

17 | 18 | Reset your password 19 | 20 |

If you did not request a password reset, please ignore this email.

21 |

Thank you!

22 |
23 | ); 24 | -------------------------------------------------------------------------------- /src/components/EmailTemplates/verify-email.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface VerifyEmailTemplateProps { 4 | firstName: string; 5 | verificationUrl: string; 6 | } 7 | 8 | export const VerifyEmailTemplate: React.FC< 9 | Readonly 10 | > = ({ firstName, verificationUrl }) => ( 11 |
12 |

Welcome, {firstName}!

13 |

14 | Thanks for signing up! Please verify your email address by clicking the 15 | link below: 16 |

17 | 21 | Verify your email 22 | 23 |

If you did not sign up for this account, please ignore this email.

24 |

Thank you!

25 |
26 | ); 27 | -------------------------------------------------------------------------------- /src/components/Header/DarkModeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import useColorMode from "@/hooks/useColorMode"; 2 | 3 | const DarkModeSwitcher = () => { 4 | const [colorMode, setColorMode] = useColorMode(); 5 | 6 | return ( 7 | 60 | ); 61 | }; 62 | 63 | export default DarkModeSwitcher; 64 | -------------------------------------------------------------------------------- /src/components/Header/DropdownMessage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import ClickOutside from "@/components/ClickOutside"; 5 | import { MessageCircleMore } from "lucide-react"; 6 | 7 | const DropdownMessage = () => { 8 | const [dropdownOpen, setDropdownOpen] = useState(false); 9 | const [notifying, setNotifying] = useState(true); 10 | 11 | return ( 12 | setDropdownOpen(false)} className="relative"> 13 |
  • 14 | { 16 | setNotifying(false); 17 | setDropdownOpen(!dropdownOpen); 18 | }} 19 | className="relative flex h-8.5 w-8.5 items-center justify-center rounded-full border-[0.5px] border-stroke bg-gray hover:text-primary dark:border-strokedark dark:bg-meta-4 dark:text-white" 20 | href="/message" 21 | > 22 | 27 | 28 | 29 | 30 | 31 |
  • 32 |
    33 | ); 34 | }; 35 | 36 | export default DropdownMessage; 37 | -------------------------------------------------------------------------------- /src/components/Header/DropdownUser.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from "react"; 3 | import { useUser } from "@/app/context/UserContext"; 4 | import { signOut } from "next-auth/react"; 5 | import { useRouter } from "next/navigation"; 6 | import { ChevronDown, LogOut, Settings, User2 } from "lucide-react"; 7 | import Image from "next/image"; 8 | import Link from "next/link"; 9 | import ClickOutside from "@/components/ClickOutside"; 10 | 11 | const DropdownUser = () => { 12 | const [dropdownOpen, setDropdownOpen] = useState(false); 13 | const user = useUser(); 14 | const router = useRouter(); 15 | 16 | const handleLogout = async () => { 17 | await signOut({ redirect: false }); 18 | router.push("/auth-page/signin"); 19 | }; 20 | 21 | return ( 22 | setDropdownOpen(false)} className="relative"> 23 | setDropdownOpen(!dropdownOpen)} 25 | className="flex items-center gap-4" 26 | href="#" 27 | > 28 | 29 | 30 | {user.firstName} {user.lastName} 31 | 32 | {user.jobTitle} 33 | 34 | 35 | 36 | User 47 | 48 | 49 | 50 | 51 | 52 | {dropdownOpen && ( 53 |
    54 |
      55 |
    • 56 | 60 | 61 | My Profile 62 | 63 |
    • 64 |
    • 65 | 69 | 70 | Account Settings 71 | 72 |
    • 73 |
    74 | 81 |
    82 | )} 83 |
    84 | ); 85 | }; 86 | 87 | export default DropdownUser; 88 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import DarkModeSwitcher from "./DarkModeSwitcher"; 3 | import DropdownMessage from "./DropdownMessage"; 4 | import DropdownUser from "./DropdownUser"; 5 | import Image from "next/image"; 6 | import { SearchIcon } from "lucide-react"; 7 | 8 | const Header = (props: { 9 | sidebarOpen: string | boolean | undefined; 10 | setSidebarOpen: (arg0: boolean) => void; 11 | }) => { 12 | return ( 13 |
    14 |
    15 |
    16 | 56 |
    57 | 58 |
    59 |
    60 |
    61 | 64 | 65 | 70 |
    71 |
    72 |
    73 | 74 |
    75 |
      76 | 77 |
    78 | 79 | 80 |
    81 |
    82 |
    83 | ); 84 | }; 85 | 86 | export default Header; 87 | -------------------------------------------------------------------------------- /src/components/Layouts/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useLayoutEffect } from "react"; 3 | import Sidebar from "@/components/Sidebar"; 4 | import Header from "@/components/Header"; 5 | import { useSession } from "next-auth/react"; 6 | import { useRouter, usePathname } from "next/navigation"; 7 | 8 | export default function DefaultLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | const [sidebarOpen, setSidebarOpen] = useState(false); 14 | 15 | const { data: session, status } = useSession(); 16 | const router = useRouter(); 17 | const pathname = usePathname(); 18 | 19 | const publicRoutes = [ 20 | "/auth-page/signin", 21 | "/auth-page/signup", 22 | "/verify-email", 23 | "/reset-password", 24 | ]; 25 | 26 | useLayoutEffect(() => { 27 | if (status === "unauthenticated" && !publicRoutes.includes(pathname)) { 28 | router.push("/auth-page/signin"); 29 | } 30 | }, [status, router, pathname]); 31 | 32 | return ( 33 | <> 34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 | {children} 41 |
    42 |
    43 |
    44 |
    45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/MoleculeBank/MoleculeBankTable.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useEffect } from "react"; 3 | import MoleculeStructure from "../MoleculeStructure/index"; 4 | 5 | const moleculeBank = [ 6 | { 7 | moleculeName: "Aspirin", 8 | smilesStructure: "CC(=O)OC1=CC=CC=C1C(O)=O", 9 | molecularWeight: 180.16, 10 | categoryUsage: "Pain reliever/NSAID", 11 | }, 12 | { 13 | moleculeName: "Caffeine", 14 | smilesStructure: "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", 15 | molecularWeight: 194.19, 16 | categoryUsage: "Stimulant", 17 | }, 18 | { 19 | moleculeName: "Benzene", 20 | smilesStructure: "C1=CC=CC=C1", 21 | molecularWeight: 78.11, 22 | categoryUsage: "Industrial solvent", 23 | }, 24 | { 25 | moleculeName: "Glucose", 26 | smilesStructure: "C(C1C(C(C(C(O1)O)O)O)O)O", 27 | molecularWeight: 180.16, 28 | categoryUsage: "Energy source/sugar", 29 | }, 30 | { 31 | moleculeName: "Penicillin", 32 | smilesStructure: "CC1(C2C(C(C(O2)N1C(=O)COC(=O)C)C)S)C=O", 33 | molecularWeight: 334.39, 34 | categoryUsage: "Antibiotic", 35 | }, 36 | { 37 | moleculeName: "Ibuprofen", 38 | smilesStructure: "CC(C)CC1=CC=C(C=C1)C(C)C(=O)O", 39 | molecularWeight: 206.28, 40 | categoryUsage: "Pain reliever/NSAID", 41 | }, 42 | { 43 | moleculeName: "Acetaminophen", 44 | smilesStructure: "CC(=O)NC1=CC=C(O)C=C1", 45 | molecularWeight: 151.16, 46 | categoryUsage: "Pain reliever/Antipyretic", 47 | }, 48 | { 49 | moleculeName: "Morphine", 50 | smilesStructure: "CN1CCC23C4C1CC(C2C3O)OC5=CC=CC=C45", 51 | molecularWeight: 285.34, 52 | categoryUsage: "Pain reliever/Opiate", 53 | }, 54 | { 55 | moleculeName: "Nicotine", 56 | smilesStructure: "CN1CCCC1C2=CN=CC=C2", 57 | molecularWeight: 162.23, 58 | categoryUsage: "Stimulant", 59 | }, 60 | { 61 | moleculeName: "Ethanol", 62 | smilesStructure: "CCO", 63 | molecularWeight: 46.07, 64 | categoryUsage: "Alcohol/Disinfectant", 65 | }, 66 | ]; 67 | 68 | const TableOne = () => { 69 | const [searchQuery, setSearchQuery] = useState(""); 70 | const [filteredMolecules, setFilteredMolecules] = useState(moleculeBank); 71 | 72 | useEffect(() => { 73 | const filteredData = moleculeBank.filter((molecule) => 74 | molecule.moleculeName.toLowerCase().includes(searchQuery.toLowerCase()), 75 | ); 76 | setFilteredMolecules(filteredData); 77 | }, [searchQuery]); 78 | 79 | return ( 80 |
    81 |

    82 | Molecules 83 |

    84 | 85 | setSearchQuery(e.target.value)} 90 | className="border-gray-300 text-gray-700 placeholder-gray-400 dark:border-gray-600 dark:placeholder-gray-500 text-md mb-4 w-full rounded-lg border bg-white px-4 py-3 shadow-sm outline-none focus:border-primary focus:ring-primary dark:bg-[#181818] dark:text-white" 91 | /> 92 |
    93 |
    94 |
    95 |
    96 | Molecule name 97 |
    98 |
    99 |
    100 |
    101 | Smile Structure Image 102 |
    103 |
    104 |
    105 |
    106 | Molecular Weights (g/mol) 107 |
    108 |
    109 |
    110 |
    111 | Category Usage 112 |
    113 |
    114 |
    115 | 116 | {filteredMolecules.map((molecule, key) => ( 117 |
    125 |
    126 |

    127 | {molecule.moleculeName} 128 |

    129 |
    130 | 131 |
    132 |
    133 | 137 |
    138 |
    139 | 140 |
    141 |

    142 | {molecule.molecularWeight} 143 |

    144 |
    145 | 146 |
    147 |

    148 | {molecule.categoryUsage} 149 |

    150 |
    151 |
    152 | ))} 153 |
    154 |
    155 | ); 156 | }; 157 | 158 | export default TableOne; 159 | -------------------------------------------------------------------------------- /src/components/MoleculeStructure/index.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { Component } from "react"; 3 | import _ from "lodash"; 4 | import PropTypes from "prop-types"; 5 | import initRDKitModule from "@rdkit/rdkit"; 6 | 7 | const initRDKit = (() => { 8 | let rdkitLoadingPromise; 9 | return () => { 10 | if (!rdkitLoadingPromise) { 11 | rdkitLoadingPromise = new Promise((resolve, reject) => { 12 | initRDKitModule() 13 | .then((RDKit) => { 14 | resolve(RDKit); 15 | }) 16 | .catch((e) => { 17 | reject(e); 18 | }); 19 | }); 20 | } 21 | return rdkitLoadingPromise; 22 | }; 23 | })(); 24 | 25 | class MoleculeStructure extends Component { 26 | static propTypes = { 27 | id: PropTypes.string.isRequired, 28 | className: PropTypes.string, 29 | svgMode: PropTypes.bool, 30 | width: PropTypes.number, 31 | height: PropTypes.number, 32 | structure: PropTypes.string.isRequired, 33 | subStructure: PropTypes.string, 34 | extraDetails: PropTypes.object, 35 | drawingDelay: PropTypes.number, 36 | scores: PropTypes.number, 37 | }; 38 | 39 | static defaultProps = { 40 | subStructure: "", 41 | className: "", 42 | width: 250, 43 | height: 200, 44 | svgMode: false, 45 | extraDetails: {}, 46 | drawingDelay: undefined, 47 | scores: 0, 48 | }; 49 | 50 | constructor(props) { 51 | super(props); 52 | 53 | this.MOL_DETAILS = { 54 | width: this.props.width, 55 | height: this.props.height, 56 | bondLineWidth: 1, 57 | addStereoAnnotation: true, 58 | ...this.props.extraDetails, 59 | }; 60 | 61 | this.state = { 62 | svg: undefined, 63 | rdKitLoaded: false, 64 | rdKitError: false, 65 | }; 66 | } 67 | 68 | drawOnce = (() => { 69 | let wasCalled = false; 70 | 71 | return () => { 72 | if (!wasCalled) { 73 | wasCalled = true; 74 | this.draw(); 75 | } 76 | }; 77 | })(); 78 | 79 | draw() { 80 | if (this.props.drawingDelay) { 81 | setTimeout(() => { 82 | this.drawSVGorCanvas(); 83 | }, this.props.drawingDelay); 84 | } else { 85 | this.drawSVGorCanvas(); 86 | } 87 | } 88 | 89 | drawSVGorCanvas() { 90 | const mol = this.RDKit.get_mol(this.props.structure || "invalid"); 91 | const qmol = this.RDKit.get_qmol(this.props.subStructure || "invalid"); 92 | const isValidMol = this.isValidMol(mol); 93 | 94 | if (this.props.svgMode && isValidMol) { 95 | const svg = mol.get_svg_with_highlights(this.getMolDetails(mol, qmol)); 96 | this.setState({ svg }); 97 | } else if (isValidMol) { 98 | const canvas = document.getElementById(this.props.id); 99 | mol.draw_to_canvas_with_highlights(canvas, this.getMolDetails(mol, qmol)); 100 | } 101 | 102 | mol?.delete(); 103 | qmol?.delete(); 104 | } 105 | 106 | isValidMol(mol) { 107 | return !!mol; 108 | } 109 | 110 | getMolDetails(mol, qmol) { 111 | if (this.isValidMol(mol) && this.isValidMol(qmol)) { 112 | const subStructHighlightDetails = JSON.parse( 113 | mol.get_substruct_matches(qmol), 114 | ); 115 | const subStructHighlightDetailsMerged = !_.isEmpty( 116 | subStructHighlightDetails, 117 | ) 118 | ? subStructHighlightDetails.reduce( 119 | (acc, { atoms, bonds }) => ({ 120 | atoms: [...acc.atoms, ...atoms], 121 | bonds: [...acc.bonds, ...bonds], 122 | }), 123 | { bonds: [], atoms: [] }, 124 | ) 125 | : subStructHighlightDetails; 126 | return JSON.stringify({ 127 | ...this.MOL_DETAILS, 128 | ...(this.props.extraDetails || {}), 129 | ...subStructHighlightDetailsMerged, 130 | }); 131 | } else { 132 | return JSON.stringify({ 133 | ...this.MOL_DETAILS, 134 | ...(this.props.extraDetails || {}), 135 | }); 136 | } 137 | } 138 | 139 | componentDidMount() { 140 | initRDKit() 141 | .then((RDKit) => { 142 | this.RDKit = RDKit; 143 | this.setState({ rdKitLoaded: true }); 144 | try { 145 | this.draw(); 146 | } catch (err) { 147 | console.log(err); 148 | } 149 | }) 150 | .catch((err) => { 151 | console.log(err); 152 | this.setState({ rdKitError: true }); 153 | }); 154 | } 155 | 156 | componentDidUpdate(prevProps) { 157 | if ( 158 | !this.state.rdKitError && 159 | this.state.rdKitLoaded && 160 | !this.props.svgMode 161 | ) { 162 | this.drawOnce(); 163 | } 164 | 165 | if (this.state.rdKitLoaded) { 166 | const shouldUpdateDrawing = 167 | prevProps.structure !== this.props.structure || 168 | prevProps.svgMode !== this.props.svgMode || 169 | prevProps.subStructure !== this.props.subStructure || 170 | prevProps.width !== this.props.width || 171 | prevProps.height !== this.props.height || 172 | !_.isEqual(prevProps.extraDetails, this.props.extraDetails); 173 | 174 | if (shouldUpdateDrawing) { 175 | this.draw(); 176 | } 177 | } 178 | } 179 | 180 | render() { 181 | console.log("props score number:", this.props.scores); 182 | if (this.state.rdKitError) { 183 | return "Error loading renderer."; 184 | } 185 | if (!this.state.rdKitLoaded) { 186 | return "Loading renderer..."; 187 | } 188 | 189 | const mol = this.RDKit.get_mol(this.props.structure || "invalid"); 190 | const isValidMol = this.isValidMol(mol); 191 | mol?.delete(); 192 | 193 | if (!isValidMol) { 194 | return ( 195 | 196 | Render Error. 197 | 198 | ); 199 | } else if (this.props.svgMode) { 200 | return ( 201 |
    207 | ); 208 | } else { 209 | return ( 210 |
    215 | 221 | {this.props.scores ? ( 222 |

    223 | Score: {this.props.scores.toFixed(2)} 224 |

    225 | ) : ( 226 | "" 227 | )} 228 |
    229 | ); 230 | } 231 | } 232 | } 233 | 234 | export default MoleculeStructure; 235 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { usePathname } from "next/navigation"; 4 | 5 | const SidebarDropdown = ({ item }: any) => { 6 | const pathname = usePathname(); 7 | 8 | return ( 9 | <> 10 |
      11 | {item.map((item: any, index: number) => ( 12 |
    • 13 | 19 | {item.label} 20 | 21 |
    • 22 | ))} 23 |
    24 | 25 | ); 26 | }; 27 | 28 | export default SidebarDropdown; 29 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import SidebarDropdown from "@/components/Sidebar/SidebarDropdown"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | const SidebarItem = ({ item, pageName, setPageName }: any) => { 7 | const handleClick = () => { 8 | const updatedPageName = 9 | pageName !== item.label.toLowerCase() ? item.label.toLowerCase() : ""; 10 | return setPageName(updatedPageName); 11 | }; 12 | 13 | const pathname = usePathname(); 14 | 15 | const isActive = (item: any) => { 16 | if (item.route === pathname) return true; 17 | if (item.children) { 18 | return item.children.some((child: any) => isActive(child)); 19 | } 20 | return false; 21 | }; 22 | 23 | const isItemActive = isActive(item); 24 | 25 | return ( 26 | <> 27 |
  • 28 | 33 | {item.icon} 34 | {item.label} 35 | 36 | 37 | {item.children && ( 38 |
    43 | 44 |
    45 | )} 46 |
  • 47 | 48 | ); 49 | }; 50 | 51 | export default SidebarItem; 52 | -------------------------------------------------------------------------------- /src/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect, useRef, useState } from "react"; 4 | import { usePathname } from "next/navigation"; 5 | import Link from "next/link"; 6 | import Image from "next/image"; 7 | import SidebarItem from "@/components/Sidebar/SidebarItem"; 8 | import ClickOutside from "@/components/ClickOutside"; 9 | import useLocalStorage from "@/hooks/useLocalStorage"; 10 | import { 11 | LayoutGrid, 12 | Atom, 13 | Network, 14 | Microscope, 15 | Settings, 16 | MessageSquareText, 17 | ChevronLeft, 18 | } from "lucide-react"; 19 | interface SidebarProps { 20 | sidebarOpen: boolean; 21 | setSidebarOpen: (arg: boolean) => void; 22 | } 23 | 24 | const menuGroups = [ 25 | { 26 | name: "", 27 | menuItems: [ 28 | { 29 | icon: , 30 | label: "Dashboard", 31 | route: "/", 32 | }, 33 | { 34 | icon: , 35 | label: "Molecules Bank", 36 | route: "/molecule-bank", 37 | }, 38 | { 39 | icon: , 40 | label: "Model", 41 | route: "/model", 42 | }, 43 | { 44 | icon: , 45 | label: "Research", 46 | route: "/research", 47 | }, 48 | { 49 | icon: , 50 | label: "Messages", 51 | route: "/message", 52 | }, 53 | ], 54 | }, 55 | { 56 | name: "OTHERS", 57 | menuItems: [ 58 | { 59 | icon: , 60 | label: "Settings", 61 | route: "/settings", 62 | }, 63 | ], 64 | }, 65 | ]; 66 | 67 | const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => { 68 | const pathname = usePathname(); 69 | const [pageName, setPageName] = useLocalStorage("selectedMenu", "dashboard"); 70 | 71 | return ( 72 | setSidebarOpen(false)}> 73 | 126 | 127 | ); 128 | }; 129 | 130 | export default Sidebar; 131 | -------------------------------------------------------------------------------- /src/components/common/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | const Loader = () => { 2 | return ( 3 |
    4 |
    5 |
    6 | ); 7 | }; 8 | 9 | export default Loader; 10 | -------------------------------------------------------------------------------- /src/components/dashboard/components/CTACard.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowRight } from "lucide-react"; 2 | import React, { ReactNode } from "react"; 3 | 4 | interface CTACardProps { 5 | title: string; 6 | subtitle: string; 7 | children: ReactNode; 8 | } 9 | 10 | const CTACard: React.FC = ({ title, subtitle, children }) => { 11 | return ( 12 |
    13 |
    14 | {children} 15 |
    16 | 17 |
    18 |
    19 |

    20 | {title} 21 |

    22 | {subtitle} 23 |
    24 |
    25 |
    26 | 27 | 28 | 29 |
    30 |
    31 | ); 32 | }; 33 | 34 | export default CTACard; 35 | -------------------------------------------------------------------------------- /src/components/dashboard/components/DashboardCardChat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useUser } from "@/app/context/UserContext"; 3 | import { ApexOptions } from "apexcharts"; 4 | import Image from "next/image"; 5 | import React from "react"; 6 | import ReactApexChart from "react-apexcharts"; 7 | const ChartThree: React.FC = () => { 8 | const user = useUser(); 9 | 10 | return ( 11 |
    12 |
    13 |
    14 |
    15 | Collaborate with team 16 |
    17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 | Jese image 28 | 29 |
    30 |
    31 | 32 | {user.firstName}{" "} 33 | 34 | 35 | 11:46 36 | 37 |
    38 |

    39 | That's awesome. I think we are making a pretty good progress 40 |

    41 | 42 | Delivered 43 | 44 |
    45 |
    46 |
    47 |
    48 |
    49 | ); 50 | }; 51 | 52 | export default ChartThree; 53 | -------------------------------------------------------------------------------- /src/components/dashboard/components/DashboardCardMap.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import jsVectorMap from "jsvectormap"; 3 | import "jsvectormap/dist/jsvectormap.css"; 4 | import React, { useEffect } from "react"; 5 | import "../../../js/us-aea-en"; 6 | import "../../../js/world"; 7 | 8 | const MapOne: React.FC = () => { 9 | useEffect(() => { 10 | const mapOne = new jsVectorMap({ 11 | selector: "#mapOne", 12 | map: "world", 13 | zoomButtons: true, 14 | 15 | regionStyle: { 16 | initial: { 17 | fill: "#C8D0D8", 18 | }, 19 | hover: { 20 | fillOpacity: 1, 21 | fill: "#3056D3", 22 | }, 23 | }, 24 | regionLabelStyle: { 25 | initial: { 26 | fontFamily: "Satoshi", 27 | fontWeight: "semibold", 28 | fill: "#fff", 29 | }, 30 | hover: { 31 | cursor: "pointer", 32 | }, 33 | }, 34 | 35 | labels: { 36 | regions: { 37 | render(code: string) { 38 | return code.split("-")[1]; 39 | }, 40 | }, 41 | }, 42 | }); 43 | 44 | return () => { 45 | const map = document.getElementById("mapOne"); 46 | if (map) { 47 | map.innerHTML = ""; 48 | } 49 | // mapOne.destroy(); 50 | }; 51 | }, []); 52 | 53 | return ( 54 |
    55 |

    56 | All over the world 57 |

    58 |
    59 |
    60 |
    61 |
    62 | ); 63 | }; 64 | 65 | export default MapOne; 66 | -------------------------------------------------------------------------------- /src/components/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | import React from "react"; 4 | 5 | import CTACard from "./components/CTACard"; 6 | import { AtomIcon, MessageCircle, Network, SearchIcon } from "lucide-react"; 7 | 8 | const DashboardCardMap = dynamic( 9 | () => import("@/components/Dashboard/components/DashboardCardMap"), 10 | { 11 | ssr: false, 12 | }, 13 | ); 14 | 15 | const DashboardCardChat = dynamic( 16 | () => import("@/components/Dashboard/components/DashboardCardChat"), 17 | { 18 | ssr: false, 19 | }, 20 | ); 21 | 22 | const Index: React.FC = () => { 23 | return ( 24 | <> 25 |
    26 | 27 | 28 | 29 | 33 | 34 | 35 | 39 | 40 | 41 | 45 | 46 | 47 |
    48 | 49 |
    50 | 51 | 52 |
    53 | 54 | ); 55 | }; 56 | 57 | export default Index; 58 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer base { 8 | body { 9 | @apply relative z-1 bg-whiten font-satoshi text-base font-normal text-body; 10 | } 11 | } 12 | 13 | @layer components { 14 | } 15 | 16 | @layer utilities { 17 | /* Chrome, Safari and Opera */ 18 | .no-scrollbar::-webkit-scrollbar { 19 | display: none; 20 | } 21 | 22 | .no-scrollbar { 23 | -ms-overflow-style: none; /* IE and Edge */ 24 | scrollbar-width: none; /* Firefox */ 25 | } 26 | 27 | .chat-height { 28 | @apply h-[calc(100vh_-_8.125rem)] lg:h-[calc(100vh_-_5.625rem)]; 29 | } 30 | 31 | .inbox-height { 32 | @apply h-[calc(100vh_-_8.125rem)] lg:h-[calc(100vh_-_5.625rem)]; 33 | } 34 | } 35 | 36 | /* third-party libraries CSS */ 37 | 38 | .tableCheckbox:checked ~ div span { 39 | @apply opacity-100; 40 | } 41 | 42 | .tableCheckbox:checked ~ div { 43 | @apply border-primary bg-primary; 44 | } 45 | 46 | .apexcharts-legend-text { 47 | @apply !text-body dark:!text-bodydark; 48 | } 49 | 50 | .apexcharts-text { 51 | @apply !fill-body dark:!fill-bodydark; 52 | } 53 | 54 | .apexcharts-xcrosshairs { 55 | @apply !fill-stroke dark:!fill-strokedark; 56 | } 57 | 58 | .apexcharts-gridline { 59 | @apply !stroke-stroke dark:!stroke-strokedark; 60 | } 61 | 62 | .apexcharts-series.apexcharts-pie-series path { 63 | @apply dark:!stroke-transparent; 64 | } 65 | 66 | .apexcharts-legend-series { 67 | @apply !inline-flex gap-1.5; 68 | } 69 | 70 | .apexcharts-tooltip.apexcharts-theme-light { 71 | @apply dark:!border-strokedark dark:!bg-boxdark; 72 | } 73 | 74 | .apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title { 75 | @apply dark:!border-strokedark dark:!bg-meta-4; 76 | } 77 | 78 | .apexcharts-xaxistooltip, 79 | .apexcharts-yaxistooltip { 80 | @apply dark:!border-meta-4 dark:!bg-meta-4 dark:!text-bodydark1; 81 | } 82 | 83 | .apexcharts-xaxistooltip-bottom:after { 84 | @apply !border-b-gray dark:!border-b-meta-4; 85 | } 86 | 87 | .apexcharts-xaxistooltip-bottom:before { 88 | @apply !border-b-gray dark:!border-b-meta-4; 89 | } 90 | 91 | .apexcharts-xaxistooltip-bottom { 92 | @apply !rounded !border-none !bg-gray !text-xs !font-medium !text-black dark:!text-white; 93 | } 94 | 95 | .apexcharts-tooltip-series-group { 96 | @apply !pl-1.5; 97 | } 98 | 99 | .flatpickr-wrapper { 100 | @apply w-full; 101 | } 102 | 103 | .flatpickr-months .flatpickr-prev-month:hover svg, 104 | .flatpickr-months .flatpickr-next-month:hover svg { 105 | @apply !fill-primary; 106 | } 107 | 108 | .flatpickr-calendar.arrowTop:before { 109 | @apply dark:!border-b-boxdark; 110 | } 111 | 112 | .flatpickr-calendar.arrowTop:after { 113 | @apply dark:!border-b-boxdark; 114 | } 115 | 116 | .flatpickr-calendar { 117 | @apply !p-6 dark:!bg-boxdark dark:!text-bodydark dark:!shadow-8 2xsm:!w-auto; 118 | } 119 | 120 | .flatpickr-day { 121 | @apply dark:!text-bodydark dark:hover:!border-meta-4 dark:hover:!bg-meta-4; 122 | } 123 | 124 | .flatpickr-months .flatpickr-prev-month, 125 | .flatpickr-months .flatpickr-next-month { 126 | @apply !top-7 dark:!fill-white dark:!text-white; 127 | } 128 | 129 | .flatpickr-months .flatpickr-prev-month.flatpickr-prev-month, 130 | .flatpickr-months .flatpickr-next-month.flatpickr-prev-month { 131 | @apply !left-7; 132 | } 133 | 134 | .flatpickr-months .flatpickr-prev-month.flatpickr-next-month, 135 | .flatpickr-months .flatpickr-next-month.flatpickr-next-month { 136 | @apply !right-7; 137 | } 138 | 139 | span.flatpickr-weekday, 140 | .flatpickr-months .flatpickr-month { 141 | @apply dark:!fill-white dark:!text-white; 142 | } 143 | 144 | .flatpickr-day.inRange { 145 | box-shadow: 146 | -5px 0 0 #f3f4f6, 147 | 5px 0 0 #f3f4f6 !important; 148 | @apply dark:!shadow-7; 149 | } 150 | 151 | .flatpickr-day.inRange, 152 | .flatpickr-day.prevMonthDay.inRange, 153 | .flatpickr-day.nextMonthDay.inRange, 154 | .flatpickr-day.today.inRange, 155 | .flatpickr-day.prevMonthDay.today.inRange, 156 | .flatpickr-day.nextMonthDay.today.inRange, 157 | .flatpickr-day:hover, 158 | .flatpickr-day.prevMonthDay:hover, 159 | .flatpickr-day.nextMonthDay:hover, 160 | .flatpickr-day:focus, 161 | .flatpickr-day.prevMonthDay:focus, 162 | .flatpickr-day.nextMonthDay:focus { 163 | @apply !border-[#F3F4F6] !bg-[#F3F4F6] dark:!border-meta-4 dark:!bg-meta-4; 164 | } 165 | 166 | .flatpickr-day.selected, 167 | .flatpickr-day.startRange, 168 | .flatpickr-day.selected, 169 | .flatpickr-day.endRange { 170 | @apply dark:!text-white; 171 | } 172 | 173 | .flatpickr-day.selected, 174 | .flatpickr-day.startRange, 175 | .flatpickr-day.endRange, 176 | .flatpickr-day.selected.inRange, 177 | .flatpickr-day.startRange.inRange, 178 | .flatpickr-day.endRange.inRange, 179 | .flatpickr-day.selected:focus, 180 | .flatpickr-day.startRange:focus, 181 | .flatpickr-day.endRange:focus, 182 | .flatpickr-day.selected:hover, 183 | .flatpickr-day.startRange:hover, 184 | .flatpickr-day.endRange:hover, 185 | .flatpickr-day.selected.prevMonthDay, 186 | .flatpickr-day.startRange.prevMonthDay, 187 | .flatpickr-day.endRange.prevMonthDay, 188 | .flatpickr-day.selected.nextMonthDay, 189 | .flatpickr-day.startRange.nextMonthDay, 190 | .flatpickr-day.endRange.nextMonthDay { 191 | background: #3c50e0; 192 | @apply !border-primary !bg-primary hover:!border-primary hover:!bg-primary; 193 | } 194 | 195 | .flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n + 1)), 196 | .flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n + 1)), 197 | .flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n + 1)) { 198 | box-shadow: -10px 0 0 #3c50e0; 199 | } 200 | 201 | .map-btn .jvm-zoom-btn { 202 | @apply flex h-7.5 w-7.5 items-center justify-center rounded border border-stroke bg-white px-0 pb-0.5 pt-0 text-2xl leading-none text-body hover:border-primary hover:bg-primary hover:text-white dark:border-strokedark dark:bg-meta-4 dark:text-bodydark dark:hover:border-primary dark:hover:bg-primary dark:hover:text-white; 203 | } 204 | 205 | .mapOne .jvm-zoom-btn { 206 | @apply !bottom-0 !left-auto !top-auto; 207 | } 208 | 209 | .mapOne .jvm-zoom-btn.jvm-zoomin { 210 | @apply !right-10; 211 | } 212 | 213 | .mapOne .jvm-zoom-btn.jvm-zoomout { 214 | @apply !right-0; 215 | } 216 | 217 | .taskCheckbox:checked ~ .box span { 218 | @apply opacity-100; 219 | } 220 | 221 | .taskCheckbox:checked ~ p { 222 | @apply line-through; 223 | } 224 | 225 | .taskCheckbox:checked ~ .box { 226 | @apply border-primary bg-primary dark:border-primary; 227 | } 228 | 229 | .custom-input-date::-webkit-calendar-picker-indicator { 230 | background: transparent; 231 | } 232 | 233 | input[type="search"]::-webkit-search-cancel-button { 234 | @apply appearance-none; 235 | } 236 | 237 | .custom-input-date::-webkit-calendar-picker-indicator { 238 | background-position: center; 239 | background-repeat: no-repeat; 240 | background-size: 20px; 241 | } 242 | 243 | [x-cloak] { 244 | display: none !important; 245 | } 246 | -------------------------------------------------------------------------------- /src/hooks/useColorMode.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import useLocalStorage from "./useLocalStorage"; 3 | 4 | const useColorMode = () => { 5 | const [colorMode, setColorMode] = useLocalStorage("color-theme", "light"); 6 | 7 | useEffect(() => { 8 | const className = "dark"; 9 | const bodyClass = window.document.body.classList; 10 | 11 | colorMode === "dark" 12 | ? bodyClass.add(className) 13 | : bodyClass.remove(className); 14 | }, [colorMode]); 15 | 16 | return [colorMode, setColorMode]; 17 | }; 18 | 19 | export default useColorMode; 20 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useState } from "react"; 3 | 4 | type SetValue = T | ((val: T) => T); 5 | 6 | function useLocalStorage( 7 | key: string, 8 | initialValue: T, 9 | ): [T, (value: SetValue) => void] { 10 | // State to store our value 11 | // Pass initial state function to useState so logic is only executed once 12 | const [storedValue, setStoredValue] = useState(() => { 13 | try { 14 | // Get from local storage by key 15 | if (typeof window !== "undefined") { 16 | // browser code 17 | const item = window.localStorage.getItem(key); 18 | // Parse stored json or if none return initialValue 19 | return item ? JSON.parse(item) : initialValue; 20 | } 21 | } catch (error) { 22 | // If error also return initialValue 23 | console.log(error); 24 | return initialValue; 25 | } 26 | }); 27 | 28 | // useEffect to update local storage when the state changes 29 | useEffect(() => { 30 | try { 31 | // Allow value to be a function so we have same API as useState 32 | const valueToStore = 33 | typeof storedValue === "function" 34 | ? storedValue(storedValue) 35 | : storedValue; 36 | // Save state 37 | if (typeof window !== "undefined") { 38 | // browser code 39 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 40 | } 41 | } catch (error) { 42 | // A more advanced implementation would handle the error case 43 | console.log(error); 44 | } 45 | }, [key, storedValue]); 46 | 47 | return [storedValue, setStoredValue]; 48 | } 49 | 50 | export default useLocalStorage; 51 | -------------------------------------------------------------------------------- /src/lib/actions/email.actions.ts: -------------------------------------------------------------------------------- 1 | export async function sendVerificationEmail( 2 | email: string, 3 | firstName: string, 4 | verificationUrl: string, 5 | ) { 6 | const response = await fetch( 7 | `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/send-verification-email`, 8 | { 9 | method: "POST", 10 | headers: { 11 | "Content-Type": "application/json", 12 | }, 13 | body: JSON.stringify({ email, firstName, verificationUrl }), 14 | }, 15 | ); 16 | 17 | const data = await response.json(); 18 | 19 | if (!response.ok) { 20 | throw new Error(data.error || "Failed to send verification email"); 21 | } 22 | 23 | return data; 24 | } 25 | 26 | export async function sendResetPasswordEmail( 27 | email: string, 28 | firstName: string, 29 | resetUrl: string, 30 | ) { 31 | const response = await fetch( 32 | `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/send-reset-password-email`, 33 | { 34 | method: "POST", 35 | headers: { 36 | "Content-Type": "application/json", 37 | }, 38 | body: JSON.stringify({ email, firstName, resetUrl }), 39 | }, 40 | ); 41 | 42 | const data = await response.json(); 43 | 44 | if (!response.ok) { 45 | throw new Error(data.error || "Failed to send reset password email"); 46 | } 47 | 48 | return data; 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/actions/group.actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import mongoose from "mongoose"; 3 | import Group from "../database/models/group.model"; 4 | import { connectToDatabase } from "../database/mongoose"; 5 | import { handleError } from "../utils"; 6 | 7 | export async function createGroup( 8 | groupName: string, 9 | creatorId: string, 10 | memberIds: string[] = [], 11 | ) { 12 | try { 13 | await connectToDatabase(); 14 | 15 | const members = Array.from( 16 | new Set([ 17 | creatorId, 18 | ...memberIds.map((id) => new mongoose.Types.ObjectId(id)), 19 | ]), 20 | ); 21 | 22 | const newGroup = await Group.create({ 23 | name: groupName, 24 | createdBy: creatorId, 25 | members, 26 | }); 27 | 28 | return JSON.parse(JSON.stringify(newGroup)); 29 | } catch (error) { 30 | console.error("Error creating group:", error); 31 | handleError(error); 32 | } 33 | } 34 | 35 | export async function addMemberToGroup(groupId: string, userId: string) { 36 | try { 37 | await connectToDatabase(); 38 | 39 | const group = await Group.findById(groupId); 40 | if (!group) throw new Error("Group not found"); 41 | 42 | if (!group.members.includes(userId)) { 43 | group.members.push(userId); 44 | await group.save(); 45 | } 46 | 47 | return JSON.parse(JSON.stringify(group)); 48 | } catch (error) { 49 | console.error("Error adding member to group:", error); 50 | handleError(error); 51 | } 52 | } 53 | 54 | export async function addMessageToGroup( 55 | groupId: string, 56 | userId: string, 57 | text: string, 58 | ) { 59 | try { 60 | await connectToDatabase(); 61 | const group = await Group.findById(groupId); 62 | if (!group) throw new Error("Group not found"); 63 | 64 | const message = { 65 | sender: userId, 66 | text, 67 | }; 68 | 69 | group.messages.push(message); 70 | await group.save(); 71 | 72 | return JSON.parse(JSON.stringify(group)); 73 | } catch (error) { 74 | console.error("Error adding message to group:", error); 75 | handleError(error); 76 | } 77 | } 78 | 79 | export async function getGroupById(groupId: string) { 80 | try { 81 | await connectToDatabase(); 82 | 83 | const group = await Group.findById(groupId).populate( 84 | "members", 85 | "firstName lastName photo", 86 | ); 87 | if (!group) throw new Error("Group not found"); 88 | 89 | return JSON.parse(JSON.stringify(group)); 90 | } catch (error) { 91 | console.error("Error retrieving group:", error); 92 | handleError(error); 93 | } 94 | } 95 | 96 | export async function getAllGroups() { 97 | try { 98 | await connectToDatabase(); 99 | 100 | const groups = await Group.find().populate( 101 | "members", 102 | "firstName lastName photo", 103 | ); 104 | 105 | return JSON.parse(JSON.stringify(groups)); 106 | } catch (error) { 107 | console.error("Error retrieving groups:", error); 108 | handleError(error); 109 | } 110 | } 111 | 112 | export async function getGroupMessages(groupId: string) { 113 | try { 114 | await connectToDatabase(); 115 | 116 | const group = await Group.findById(groupId).populate( 117 | "messages.sender", 118 | "firstName lastName photo", 119 | ); 120 | if (!group) throw new Error("Group not found"); 121 | 122 | return JSON.parse(JSON.stringify(group.messages)); 123 | } catch (error) { 124 | console.error("Error retrieving group messages:", error); 125 | handleError(error); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/lib/actions/molecule-generation.action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import MoleculeGenerationHistory from "../database/models/molecule-generation.model"; 5 | import { connectToDatabase } from "../database/mongoose"; 6 | import { handleError } from "../utils"; 7 | import mongoose from "mongoose"; 8 | 9 | export async function createMoleculeGenerationHistory( 10 | payload: MoleculeGenerationHistoryType, 11 | userId: string, 12 | ) { 13 | try { 14 | await connectToDatabase(); 15 | 16 | const newHistoryEntry = await MoleculeGenerationHistory.create({ 17 | ...payload, 18 | user: new mongoose.Types.ObjectId(userId), 19 | }); 20 | 21 | return JSON.parse(JSON.stringify(newHistoryEntry)); 22 | } catch (error) { 23 | console.error("Error creating history entry:", error); 24 | handleError(error); 25 | } 26 | } 27 | 28 | export async function getMoleculeGenerationHistoryByUser(userId: string) { 29 | try { 30 | await connectToDatabase(); 31 | 32 | const historyEntries = await MoleculeGenerationHistory.find({ 33 | user: userId, 34 | }).sort({ createdAt: -1 }); 35 | 36 | return JSON.parse(JSON.stringify(historyEntries)); 37 | } catch (error) { 38 | console.error("Error retrieving history entries:", error); 39 | handleError(error); 40 | } 41 | } 42 | 43 | export async function getMoleculeGenerationHistoryById(historyId: string) { 44 | try { 45 | await connectToDatabase(); 46 | 47 | const historyEntry = await MoleculeGenerationHistory.findById(historyId); 48 | if (!historyEntry) throw new Error("History entry not found"); 49 | 50 | return JSON.parse(JSON.stringify(historyEntry)); 51 | } catch (error) { 52 | console.error("Error retrieving history entry by ID:", error); 53 | handleError(error); 54 | } 55 | } 56 | 57 | export async function deleteMoleculeGenerationHistory(entryId: string) { 58 | try { 59 | await connectToDatabase(); 60 | 61 | const deletedEntry = 62 | await MoleculeGenerationHistory.findByIdAndDelete(entryId); 63 | 64 | return JSON.parse(JSON.stringify(deletedEntry)); 65 | } catch (error) { 66 | console.error("Error deleting history entry:", error); 67 | handleError(error); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/actions/user.actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import bcrypt from "bcrypt"; 5 | import User from "../database/models/user.model"; 6 | import { connectToDatabase } from "../database/mongoose"; 7 | import { handleError } from "../utils"; 8 | import { sendVerificationEmail, sendResetPasswordEmail } from "./email.actions"; 9 | 10 | export async function createUser(user: CreateUserParams) { 11 | try { 12 | await connectToDatabase(); 13 | 14 | const existingUser = await User.findOne({ email: user.email }); 15 | if (existingUser) { 16 | throw new Error("User already exists with this email"); 17 | } 18 | 19 | const salt = await bcrypt.genSalt(10); 20 | const hashedPassword = await bcrypt.hash(user.password, salt); 21 | 22 | const newUser = await User.create({ 23 | ...user, 24 | password: hashedPassword, 25 | userBio: user.userBio || "", 26 | }); 27 | 28 | const verificationUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/verify-email?token=${newUser._id}`; 29 | await sendVerificationEmail( 30 | newUser.email, 31 | newUser.firstName || "User", 32 | verificationUrl, 33 | ); 34 | 35 | return JSON.parse(JSON.stringify(newUser)); 36 | } catch (error: any) { 37 | console.log(error); 38 | handleError(error); 39 | throw new Error( 40 | error.message || "An error occurred during user registration", 41 | ); 42 | } 43 | } 44 | 45 | export async function loginUser(email: string, password: string) { 46 | try { 47 | await connectToDatabase(); 48 | 49 | const user = await User.findOne({ email }); 50 | if (!user) throw new Error("Invalid credentials"); 51 | 52 | const isMatch = await bcrypt.compare(password, user.password); 53 | if (!isMatch) throw new Error("Invalid credentials"); 54 | 55 | return JSON.parse(JSON.stringify(user)); 56 | } catch (error) { 57 | handleError(error); 58 | } 59 | } 60 | 61 | export async function verifyEmail(token: string) { 62 | try { 63 | await connectToDatabase(); 64 | 65 | const user = await User.findById(token); 66 | if (!user) throw new Error("Invalid token or user not found"); 67 | 68 | user.isEmailVerified = true; 69 | await user.save(); 70 | 71 | return JSON.parse(JSON.stringify(user)); 72 | } catch (error) { 73 | handleError(error); 74 | } 75 | } 76 | 77 | export async function requestPasswordReset(email: string) { 78 | try { 79 | await connectToDatabase(); 80 | 81 | const user = await User.findOne({ email }); 82 | if (!user) throw new Error("User not found"); 83 | const resetUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/reset-password?token=${user._id}`; 84 | await sendResetPasswordEmail( 85 | user.email, 86 | user.firstName || "User", 87 | resetUrl, 88 | ); 89 | 90 | return true; 91 | } catch (error) { 92 | console.error(error); 93 | throw error; 94 | } 95 | } 96 | 97 | export async function resetPassword(token: string, newPassword: string) { 98 | try { 99 | await connectToDatabase(); 100 | 101 | const user = await User.findById(token); 102 | if (!user) throw new Error("Invalid token or user not found"); 103 | 104 | const salt = await bcrypt.genSalt(10); 105 | const hashedPassword = await bcrypt.hash(newPassword, salt); 106 | 107 | user.password = hashedPassword; 108 | await user.save(); 109 | 110 | return JSON.parse(JSON.stringify(user)); 111 | } catch (error) { 112 | handleError(error); 113 | } 114 | } 115 | 116 | export async function getUserById(userId: string) { 117 | try { 118 | await connectToDatabase(); 119 | const user = await User.findOne({ Id: userId }); 120 | if (!user) throw new Error("User not found"); 121 | return JSON.parse(JSON.stringify(user)); 122 | } catch (error) { 123 | handleError(error); 124 | } 125 | } 126 | 127 | export async function updateUser(Id: string, user: UpdateUserParams) { 128 | try { 129 | await connectToDatabase(); 130 | const updatedUser = await User.findOneAndUpdate({ _id: Id }, user, { 131 | new: true, 132 | }); 133 | if (!updatedUser) throw new Error("User update failed"); 134 | return JSON.parse(JSON.stringify(updatedUser)); 135 | } catch (error) { 136 | handleError(error); 137 | } 138 | } 139 | 140 | export async function deleteUser(Id: string) { 141 | try { 142 | await connectToDatabase(); 143 | 144 | const userToDelete = await User.findOne({ Id }); 145 | if (!userToDelete) { 146 | throw new Error("User not found"); 147 | } 148 | 149 | const deletedUser = await User.findByIdAndDelete(userToDelete._id); 150 | revalidatePath("/"); 151 | 152 | return deletedUser ? JSON.parse(JSON.stringify(deletedUser)) : null; 153 | } catch (error) { 154 | handleError(error); 155 | } 156 | } 157 | 158 | export async function updateCredits(userId: string, creditFee: number) { 159 | try { 160 | await connectToDatabase(); 161 | 162 | const updatedUserCredits = await User.findOneAndUpdate( 163 | { _id: userId }, 164 | { $inc: { creditBalance: creditFee } }, 165 | { new: true }, 166 | ); 167 | 168 | if (!updatedUserCredits) throw new Error("User credits update failed"); 169 | 170 | return JSON.parse(JSON.stringify(updatedUserCredits)); 171 | } catch (error) { 172 | handleError(error); 173 | } 174 | } 175 | 176 | export async function getUserByEmail(email: string) { 177 | try { 178 | await connectToDatabase(); 179 | const user = await User.findOne({ email }); 180 | if (!user) throw new Error("User not found"); 181 | 182 | return JSON.parse(JSON.stringify(user)); 183 | } catch (error) { 184 | handleError(error); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/lib/database/models/group.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | 3 | const MessageSchema = new Schema({ 4 | sender: { 5 | type: Schema.Types.ObjectId, 6 | ref: "User", 7 | required: true, 8 | }, 9 | text: { 10 | type: String, 11 | required: true, 12 | }, 13 | timestamp: { 14 | type: Date, 15 | default: Date.now, 16 | }, 17 | }); 18 | 19 | const GroupSchema = new Schema({ 20 | createdAt: { 21 | type: Date, 22 | default: Date.now, 23 | }, 24 | name: { 25 | type: String, 26 | required: true, 27 | unique: true, 28 | }, 29 | createdBy: { 30 | type: Schema.Types.ObjectId, 31 | ref: "User", 32 | required: true, 33 | }, 34 | members: [ 35 | { 36 | type: Schema.Types.ObjectId, 37 | ref: "User", 38 | }, 39 | ], 40 | messages: [MessageSchema], 41 | }); 42 | 43 | const Group = models?.Group || model("Group", GroupSchema); 44 | 45 | export default Group; 46 | -------------------------------------------------------------------------------- /src/lib/database/models/molecule-generation.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | 3 | const MoleculeGenerationHistorySchema = new Schema( 4 | { 5 | smiles: { 6 | type: String, 7 | required: true, 8 | }, 9 | numMolecules: { 10 | type: Number, 11 | required: true, 12 | }, 13 | minSimilarity: { 14 | type: Number, 15 | required: true, 16 | }, 17 | particles: { 18 | type: Number, 19 | required: true, 20 | }, 21 | iterations: { 22 | type: Number, 23 | required: true, 24 | }, 25 | generatedMolecules: [ 26 | { 27 | structure: { type: String, required: true }, 28 | score: { type: Number, required: true }, 29 | }, 30 | ], 31 | user: { 32 | type: Schema.Types.ObjectId, 33 | ref: "User", // Reference to the user model 34 | required: true, 35 | }, 36 | createdAt: { 37 | type: Date, 38 | default: Date.now, 39 | }, 40 | }, 41 | { timestamps: true }, 42 | ); 43 | 44 | const MoleculeGenerationHistory = 45 | models.MoleculeGenerationHistory || 46 | model("MoleculeGenerationHistory", MoleculeGenerationHistorySchema); 47 | 48 | export default MoleculeGenerationHistory; 49 | -------------------------------------------------------------------------------- /src/lib/database/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | 3 | const UserSchema = new Schema( 4 | { 5 | email: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | fullname: { 11 | type: String, 12 | unique: false, 13 | required: false, 14 | }, 15 | photo: { 16 | type: String, 17 | required: true, 18 | }, 19 | firstName: { type: String }, 20 | lastName: { 21 | type: String, 22 | }, 23 | 24 | password: { 25 | type: String, 26 | required: true, 27 | }, 28 | isEmailVerified: { 29 | type: Boolean, 30 | default: false, 31 | }, 32 | userBio: { 33 | type: String, 34 | default: "", 35 | }, 36 | verificationToken: String, 37 | verificationExpires: Date, 38 | resetPasswordToken: String, 39 | resetPasswordExpires: Date, 40 | }, 41 | { 42 | timestamps: true, 43 | }, 44 | ); 45 | 46 | const User = models?.User || model("User", UserSchema); 47 | 48 | export default User; 49 | -------------------------------------------------------------------------------- /src/lib/database/mongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Mongoose } from "mongoose"; 2 | 3 | const MONGODB_URL = process.env.MONGODB_URL; 4 | 5 | console.log(MONGODB_URL); 6 | 7 | interface MongooseConnection { 8 | conn: Mongoose | null; 9 | promise: Promise | null; 10 | } 11 | 12 | let cached: MongooseConnection = (global as any).mongoose; 13 | 14 | if (!cached) { 15 | cached = (global as any).mongoose = { 16 | conn: null, 17 | promise: null, 18 | }; 19 | } 20 | 21 | export const connectToDatabase = async () => { 22 | if (cached.conn) return cached.conn; 23 | 24 | if (!MONGODB_URL) throw new Error("Missing MONGODB_URL"); 25 | 26 | cached.promise = 27 | cached.promise || 28 | mongoose.connect(MONGODB_URL, { 29 | dbName: "proteinBind", 30 | bufferCommands: false, 31 | }); 32 | 33 | cached.conn = await cached.promise; 34 | 35 | return cached.conn; 36 | }; 37 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const handleError = (error: unknown) => { 2 | if (error instanceof Error) { 3 | console.error(error.message); 4 | throw new Error(`Error: ${error.message}`); 5 | } else if (typeof error === "string") { 6 | console.error(error); 7 | throw new Error(`Error: ${error}`); 8 | } else { 9 | console.error(error); 10 | throw new Error(`Unknown error: ${JSON.stringify(error)}`); 11 | } 12 | }; 13 | 14 | export const resizeBase64Img = ( 15 | base64Str: any, 16 | maxWidth = 100, 17 | maxHeight = 100, 18 | ) => { 19 | return new Promise((resolve) => { 20 | const img = new Image(); 21 | img.src = base64Str; 22 | 23 | img.onload = () => { 24 | const canvas = document.createElement("canvas"); 25 | let width = img.width; 26 | let height = img.height; 27 | 28 | if (width > height) { 29 | if (width > maxWidth) { 30 | height = Math.round((height * maxWidth) / width); 31 | width = maxWidth; 32 | } 33 | } else { 34 | if (height > maxHeight) { 35 | width = Math.round((width * maxHeight) / height); 36 | height = maxHeight; 37 | } 38 | } 39 | 40 | canvas.width = width; 41 | canvas.height = height; 42 | 43 | const ctx = canvas.getContext("2d") as any; 44 | ctx.drawImage(img, 0, 0, width, height); 45 | 46 | const newBase64Str = canvas.toDataURL("image/jpeg", 0.7); 47 | resolve(newBase64Str); 48 | }; 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare type MoleculeStructure = { 2 | structure: string; 3 | score: number; 4 | }; 5 | 6 | declare type MoleculeGenerationHistoryType = { 7 | _id?: string; 8 | smiles: string; 9 | numMolecules: number; 10 | minSimilarity: number; 11 | particles: number; 12 | iterations: number; 13 | generatedMolecules: MoleculeStructure[]; 14 | createdAt?: Date; 15 | }; 16 | 17 | declare type CreateUserParams = { 18 | email: string; 19 | fullname?: string; 20 | password: string; 21 | photo: string; 22 | firstName?: string; 23 | lastName?: string; 24 | userBio?: string; 25 | isEmailVerified?: boolean; 26 | }; 27 | 28 | declare type UpdateUserParams = { 29 | firstName: string; 30 | lastName: string; 31 | photo: string; 32 | userBio?: string; 33 | email: string; 34 | }; 35 | 36 | declare type CompoundData = { 37 | MolecularFormula: string; 38 | MolecularWeight: string; 39 | InChIKey: string; 40 | CanonicalSMILES: string; 41 | IsomericSMILES: string; 42 | IUPACName: string; 43 | XLogP: string; 44 | ExactMass: string; 45 | MonoisotopicMass: string; 46 | TPSA: string; 47 | Complexity: string; 48 | Charge: string; 49 | HBondDonorCount: string; 50 | HBondAcceptorCount: string; 51 | RotatableBondCount: string; 52 | HeavyAtomCount: string; 53 | }; 54 | 55 | declare type ModalProps = { 56 | id: string; 57 | title: string; 58 | content: React.ReactNode; 59 | onCloseText: string; 60 | }; 61 | -------------------------------------------------------------------------------- /tailadmin-nextjs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendsalbert/ProteinBind/654560d8d17542170ae40170254650993859736e/tailadmin-nextjs.jpg -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import defaultTheme from "tailwindcss/defaultTheme"; 3 | 4 | const config: Config = { 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | darkMode: "class", 11 | theme: { 12 | fontFamily: { 13 | satoshi: ["Satoshi", "sans-serif"], 14 | inter: ["Inter", "sans-serif"], 15 | poppins: ["Poppins"], 16 | }, 17 | screens: { 18 | "2xsm": "375px", 19 | xsm: "425px", 20 | "3xl": "2000px", 21 | ...defaultTheme.screens, 22 | }, 23 | extend: { 24 | colors: { 25 | current: "currentColor", 26 | transparent: "transparent", 27 | white: "#FFFFFF", 28 | black: "#1C2434", 29 | red: "#FB5454", 30 | "black-2": "#010101", 31 | body: "#64748B", 32 | bodydark: "#AEB7C0", 33 | bodydark1: "#DEE4EE", 34 | bodydark2: "#8A99AF", 35 | primary: "#3C50E0", 36 | secondary: "#80CAEE", 37 | stroke: "#E2E8F0", 38 | gray: "#EFF4FB", 39 | graydark: "#333A48", 40 | "gray-2": "#F7F9FC", 41 | "gray-3": "#FAFAFA", 42 | whiten: "#F1F5F9", 43 | whiter: "#F5F7FD", 44 | boxdark: "#24303F", 45 | "boxdark-2": "#1A222C", 46 | strokedark: "#2E3A47", 47 | "form-strokedark": "#3d4d60", 48 | "form-input": "#1d2a39", 49 | "meta-1": "#DC3545", 50 | "meta-2": "#EFF2F7", 51 | "meta-3": "#10B981", 52 | "meta-4": "#313D4A", 53 | "meta-5": "#259AE6", 54 | "meta-6": "#FFBA00", 55 | "meta-7": "#FF6766", 56 | "meta-8": "#F0950C", 57 | "meta-9": "#E5E7EB", 58 | "meta-10": "#0FADCF", 59 | success: "#219653", 60 | danger: "#D34053", 61 | warning: "#FFA70B", 62 | }, 63 | fontSize: { 64 | "title-xxl": ["44px", "55px"], 65 | "title-xxl2": ["42px", "58px"], 66 | "title-xl": ["36px", "45px"], 67 | "title-xl2": ["33px", "45px"], 68 | "title-lg": ["28px", "35px"], 69 | "title-md": ["24px", "30px"], 70 | "title-md2": ["26px", "30px"], 71 | "title-sm": ["20px", "26px"], 72 | "title-sm2": ["22px", "28px"], 73 | "title-xsm": ["18px", "24px"], 74 | }, 75 | spacing: { 76 | 4.5: "1.125rem", 77 | 5.5: "1.375rem", 78 | 6.5: "1.625rem", 79 | 7.5: "1.875rem", 80 | 8.5: "2.125rem", 81 | 9.5: "2.375rem", 82 | 10.5: "2.625rem", 83 | 11: "2.75rem", 84 | 11.5: "2.875rem", 85 | 12.5: "3.125rem", 86 | 13: "3.25rem", 87 | 13.5: "3.375rem", 88 | 14: "3.5rem", 89 | 14.5: "3.625rem", 90 | 15: "3.75rem", 91 | 15.5: "3.875rem", 92 | 16: "4rem", 93 | 16.5: "4.125rem", 94 | 17: "4.25rem", 95 | 17.5: "4.375rem", 96 | 18: "4.5rem", 97 | 18.5: "4.625rem", 98 | 19: "4.75rem", 99 | 19.5: "4.875rem", 100 | 21: "5.25rem", 101 | 21.5: "5.375rem", 102 | 22: "5.5rem", 103 | 22.5: "5.625rem", 104 | 24.5: "6.125rem", 105 | 25: "6.25rem", 106 | 25.5: "6.375rem", 107 | 26: "6.5rem", 108 | 27: "6.75rem", 109 | 27.5: "6.875rem", 110 | 29: "7.25rem", 111 | 29.5: "7.375rem", 112 | 30: "7.5rem", 113 | 31: "7.75rem", 114 | 32.5: "8.125rem", 115 | 33: "8.25rem", 116 | 34: "8.5rem", 117 | 34.5: "8.625rem", 118 | 35: "8.75rem", 119 | 36.5: "9.125rem", 120 | 37.5: "9.375rem", 121 | 39: "9.75rem", 122 | 39.5: "9.875rem", 123 | 40: "10rem", 124 | 42.5: "10.625rem", 125 | 44: "11rem", 126 | 45: "11.25rem", 127 | 46: "11.5rem", 128 | 47.5: "11.875rem", 129 | 49: "12.25rem", 130 | 50: "12.5rem", 131 | 52: "13rem", 132 | 52.5: "13.125rem", 133 | 54: "13.5rem", 134 | 54.5: "13.625rem", 135 | 55: "13.75rem", 136 | 55.5: "13.875rem", 137 | 59: "14.75rem", 138 | 60: "15rem", 139 | 62.5: "15.625rem", 140 | 65: "16.25rem", 141 | 67: "16.75rem", 142 | 67.5: "16.875rem", 143 | 70: "17.5rem", 144 | 72.5: "18.125rem", 145 | 73: "18.25rem", 146 | 75: "18.75rem", 147 | 90: "22.5rem", 148 | 94: "23.5rem", 149 | 95: "23.75rem", 150 | 100: "25rem", 151 | 115: "28.75rem", 152 | 125: "31.25rem", 153 | 132.5: "33.125rem", 154 | 150: "37.5rem", 155 | 171.5: "42.875rem", 156 | 180: "45rem", 157 | 187.5: "46.875rem", 158 | 203: "50.75rem", 159 | 230: "57.5rem", 160 | 242.5: "60.625rem", 161 | }, 162 | maxWidth: { 163 | 2.5: "0.625rem", 164 | 3: "0.75rem", 165 | 4: "1rem", 166 | 7: "1.75rem", 167 | 9: "2.25rem", 168 | 10: "2.5rem", 169 | 10.5: "2.625rem", 170 | 11: "2.75rem", 171 | 13: "3.25rem", 172 | 14: "3.5rem", 173 | 15: "3.75rem", 174 | 16: "4rem", 175 | 22.5: "5.625rem", 176 | 25: "6.25rem", 177 | 30: "7.5rem", 178 | 34: "8.5rem", 179 | 35: "8.75rem", 180 | 40: "10rem", 181 | 42.5: "10.625rem", 182 | 44: "11rem", 183 | 45: "11.25rem", 184 | 60: "15rem", 185 | 70: "17.5rem", 186 | 90: "22.5rem", 187 | 94: "23.5rem", 188 | 125: "31.25rem", 189 | 132.5: "33.125rem", 190 | 142.5: "35.625rem", 191 | 150: "37.5rem", 192 | 180: "45rem", 193 | 203: "50.75rem", 194 | 230: "57.5rem", 195 | 242.5: "60.625rem", 196 | 270: "67.5rem", 197 | 280: "70rem", 198 | 292.5: "73.125rem", 199 | }, 200 | maxHeight: { 201 | 35: "8.75rem", 202 | 70: "17.5rem", 203 | 90: "22.5rem", 204 | 550: "34.375rem", 205 | 300: "18.75rem", 206 | }, 207 | minWidth: { 208 | 22.5: "5.625rem", 209 | 42.5: "10.625rem", 210 | 47.5: "11.875rem", 211 | 75: "18.75rem", 212 | }, 213 | zIndex: { 214 | 999999: "999999", 215 | 99999: "99999", 216 | 9999: "9999", 217 | 999: "999", 218 | 99: "99", 219 | 9: "9", 220 | 1: "1", 221 | }, 222 | opacity: { 223 | 65: ".65", 224 | }, 225 | aspectRatio: { 226 | "4/3": "4 / 3", 227 | "21/9": "21 / 9", 228 | }, 229 | backgroundImage: { 230 | video: "url('../images/video/video.png')", 231 | }, 232 | content: { 233 | "icon-copy": 'url("../images/icon/icon-copy-alt.svg")', 234 | }, 235 | transitionProperty: { width: "width", stroke: "stroke" }, 236 | borderWidth: { 237 | 6: "6px", 238 | 10: "10px", 239 | 12: "12px", 240 | }, 241 | boxShadow: { 242 | default: "0px 8px 13px -3px rgba(0, 0, 0, 0.07)", 243 | card: "0px 1px 3px rgba(0, 0, 0, 0.12)", 244 | "card-2": "0px 1px 2px rgba(0, 0, 0, 0.05)", 245 | switcher: 246 | "0px 2px 4px rgba(0, 0, 0, 0.2), inset 0px 2px 2px #FFFFFF, inset 0px -1px 1px rgba(0, 0, 0, 0.1)", 247 | "switch-1": "0px 0px 5px rgba(0, 0, 0, 0.15)", 248 | 1: "0px 1px 3px rgba(0, 0, 0, 0.08)", 249 | 2: "0px 1px 4px rgba(0, 0, 0, 0.12)", 250 | 3: "0px 1px 5px rgba(0, 0, 0, 0.14)", 251 | 4: "0px 4px 10px rgba(0, 0, 0, 0.12)", 252 | 5: "0px 1px 1px rgba(0, 0, 0, 0.15)", 253 | 6: "0px 3px 15px rgba(0, 0, 0, 0.1)", 254 | 7: "-5px 0 0 #313D4A, 5px 0 0 #313D4A", 255 | 8: "1px 0 0 #313D4A, -1px 0 0 #313D4A, 0 1px 0 #313D4A, 0 -1px 0 #313D4A, 0 3px 13px rgb(0 0 0 / 8%)", 256 | 9: "0px 2px 3px rgba(183, 183, 183, 0.5)", 257 | 10: "0px 1px 2px 0px rgba(0, 0, 0, 0.10)", 258 | 11: "0px 1px 3px 0px rgba(166, 175, 195, 0.40)", 259 | 12: "0px 0.5px 3px 0px rgba(0, 0, 0, 0.18)", 260 | 13: "0px 1px 3px 0px rgba(0, 0, 0, 0.08)", 261 | 14: "0px 2px 3px 0px rgba(0, 0, 0, 0.10)", 262 | }, 263 | dropShadow: { 264 | 1: "0px 1px 0px #E2E8F0", 265 | 2: "0px 1px 4px rgba(0, 0, 0, 0.12)", 266 | 3: "0px 0px 4px rgba(0, 0, 0, 0.15)", 267 | 4: "0px 0px 2px rgba(0, 0, 0, 0.2)", 268 | 5: "0px 1px 5px rgba(0, 0, 0, 0.2)", 269 | }, 270 | keyframes: { 271 | linspin: { 272 | "100%": { transform: "rotate(360deg)" }, 273 | }, 274 | easespin: { 275 | "12.5%": { transform: "rotate(135deg)" }, 276 | "25%": { transform: "rotate(270deg)" }, 277 | "37.5%": { transform: "rotate(405deg)" }, 278 | "50%": { transform: "rotate(540deg)" }, 279 | "62.5%": { transform: "rotate(675deg)" }, 280 | "75%": { transform: "rotate(810deg)" }, 281 | "87.5%": { transform: "rotate(945deg)" }, 282 | "100%": { transform: "rotate(1080deg)" }, 283 | }, 284 | "left-spin": { 285 | "0%": { transform: "rotate(130deg)" }, 286 | "50%": { transform: "rotate(-5deg)" }, 287 | "100%": { transform: "rotate(130deg)" }, 288 | }, 289 | "right-spin": { 290 | "0%": { transform: "rotate(-130deg)" }, 291 | "50%": { transform: "rotate(5deg)" }, 292 | "100%": { transform: "rotate(-130deg)" }, 293 | }, 294 | rotating: { 295 | "0%, 100%": { transform: "rotate(360deg)" }, 296 | "50%": { transform: "rotate(0deg)" }, 297 | }, 298 | topbottom: { 299 | "0%, 100%": { transform: "translate3d(0, -100%, 0)" }, 300 | "50%": { transform: "translate3d(0, 0, 0)" }, 301 | }, 302 | bottomtop: { 303 | "0%, 100%": { transform: "translate3d(0, 0, 0)" }, 304 | "50%": { transform: "translate3d(0, -100%, 0)" }, 305 | }, 306 | line: { 307 | "0%, 100%": { transform: "translateY(0)" }, 308 | "50%": { transform: "translateY(100%)" }, 309 | }, 310 | "line-revert": { 311 | "0%, 100%": { transform: "translateY(100%)" }, 312 | "50%": { transform: "translateY(0)" }, 313 | }, 314 | }, 315 | animation: { 316 | linspin: "linspin 1568.2353ms linear infinite", 317 | easespin: "easespin 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both", 318 | "left-spin": 319 | "left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both", 320 | "right-spin": 321 | "right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both", 322 | "ping-once": "ping 5s cubic-bezier(0, 0, 0.2, 1)", 323 | rotating: "rotating 30s linear infinite", 324 | topbottom: "topbottom 60s infinite alternate linear", 325 | bottomtop: "bottomtop 60s infinite alternate linear", 326 | "spin-1.5": "spin 1.5s linear infinite", 327 | "spin-2": "spin 2s linear infinite", 328 | "spin-3": "spin 3s linear infinite", 329 | line1: "line 10s infinite linear", 330 | line2: "line-revert 8s infinite linear", 331 | line3: "line 7s infinite linear", 332 | }, 333 | }, 334 | }, 335 | plugins: [require("daisyui")], 336 | }; 337 | export default config; 338 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------