├── .gitignore
├── README.md
├── client
├── .gitignore
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── react.png
│ ├── shirt_baked.glb
│ ├── threejs.png
│ └── vite.svg
├── src
│ ├── App.jsx
│ ├── assets
│ │ ├── ai.png
│ │ ├── download.png
│ │ ├── file.png
│ │ ├── index.js
│ │ ├── logo-tshirt.png
│ │ ├── stylish-tshirt.png
│ │ └── swatch.png
│ ├── canvas
│ │ ├── Backdrop.jsx
│ │ ├── CameraRig.jsx
│ │ ├── Shirt.jsx
│ │ └── index.jsx
│ ├── components
│ │ ├── AIPicker.jsx
│ │ ├── ColorPicker.jsx
│ │ ├── CustomButton.jsx
│ │ ├── FilePicker.jsx
│ │ ├── Tab.jsx
│ │ └── index.js
│ ├── config
│ │ ├── config.js
│ │ ├── constants.js
│ │ ├── helpers.js
│ │ └── motion.js
│ ├── index.css
│ ├── main.jsx
│ ├── pages
│ │ ├── Customizer.jsx
│ │ └── Home.jsx
│ └── store
│ │ └── index.js
├── tailwind.config.js
└── vite.config.js
└── server
├── index.js
├── package.json
└── routes
└── dalle.routes.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
A 3D Dev Swag Website
15 |
16 |
17 | Build this project step by step with our detailed tutorial on
JavaScript Mastery YouTube. Join the JSM family!
18 |
19 |
20 |
21 | ## 📋 Table of Contents
22 |
23 | 1. 🤖 [Introduction](#introduction)
24 | 2. ⚙️ [Tech Stack](#tech-stack)
25 | 3. 🔋 [Features](#features)
26 | 4. 🤸 [Quick Start](#quick-start)
27 | 5. 🕸️ [Snippets](#snippets)
28 | 6. 🔗 [Links](#links)
29 | 7. 🚀 [More](#more)
30 |
31 | ## 🚨 Tutorial
32 |
33 | This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, JavaScript Mastery.
34 |
35 | 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!
36 |
37 |
38 |
39 | ## 🤖 Introduction
40 |
41 | Create your own style with our new 3D Swag Customization App. Pick colors, add logos, and try AI designs to make your virtual swag unique. Built using React.js, Three.js, and OpenAI to show the usage of a 3D world with AI
42 |
43 | If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out.
44 |
45 |
46 |
47 | ## ⚙️ Tech Stack
48 |
49 | - React.js
50 | - Three.js
51 | - React Three Fiber
52 | - React Three Drei
53 | - Vite
54 | - Tailwind CSS
55 | - Node.js
56 | - Express.js
57 | - OpenAI
58 | - Framer Motion
59 | - Valtio
60 |
61 | ## 🔋 Features
62 |
63 | 👉 **3D Swag Generation**: Generate unique 3D shirts/swag items dynamically
64 |
65 | 👉 **Color Customization**: Apply any color to the 3D shirt/swag for personalized styling.
66 |
67 | 👉 **Logo Upload Functionality**: Enable users to upload any file as a logo, integrating it seamlessly onto the 3D shirt.
68 |
69 | 👉 **Texture Image Upload**: Allow users to upload texture images to style the 3D shirt/swag.
70 |
71 | 👉 **AI-Generated Logo Integration**: Utilize AI to generate logos and intelligently apply them to the 3D shirt.
72 |
73 | 👉 **AI-Generated Textures**: Implement AI-generated textures for enhanced 3D shirt customization.
74 |
75 | 👉 **Download Options**:Dynamically change the application theme based on the selected color, enhancing user experience.
76 |
77 | 👉 **Theme Change with Color Selection**: Dynamically change the application theme based on the selected color, enhancing user experience
78 |
79 | 👉 **Responsive 3D Application**: Ensure the application is responsive, delivering a seamless experience across various devices.
80 |
81 | 👉 **Framer Motion Animation**: Implement framer motion animations for smooth transitions between different 3D models.
82 |
83 | and many more, including code architecture and reusability
84 |
85 | ## 🤸 Quick Start
86 |
87 | Follow these steps to set up the project locally on your machine.
88 |
89 | **Prerequisites**
90 |
91 | Make sure you have the following installed on your machine:
92 |
93 | - [Git](https://git-scm.com/)
94 | - [Node.js](https://nodejs.org/en)
95 | - [npm](https://www.npmjs.com/) (Node Package Manager)
96 |
97 | **Cloning the Repository**
98 |
99 | ```bash
100 | git clone https://github.com/adrianhajdin/project_threejs_ai.git
101 | cd project_threejs_ai
102 | ```
103 |
104 | **Installation**
105 |
106 | Install the project dependencies using npm in both client and server folders:
107 |
108 | ```bash
109 | npm install
110 | ```
111 |
112 | **Set Up Environment Variables**
113 |
114 | Create a new file named `.env` in the root of your project and add the following content:
115 |
116 | ```env
117 | OPENAI_API_KEY=
118 | ```
119 |
120 | Replace the placeholder values with your actual OpenAI credentials. You can obtain these credentials by signing up on the [Open website](https://openai.com/).
121 |
122 | **Running the Project**
123 |
124 | 1. Server
125 | ```bash
126 | npm start
127 | ```
128 | 2. Client
129 | ```bash
130 | npm run dev
131 | ```
132 |
133 | Open [http://localhost:5173](http://localhost:5173) in your browser to view the project.
134 |
135 | ## 🕸️ Snippets
136 |
137 |
138 | Customizer.jsx
139 |
140 | ```javascript
141 |
148 | ```
149 |
150 |
151 |
152 | index.css
153 |
154 | ```css
155 | @import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,200;0,600;1,900&display=swap");
156 | @import url("https://rsms.me/inter/inter.css");
157 |
158 | @tailwind base;
159 | @tailwind components;
160 | @tailwind utilities;
161 |
162 | html {
163 | font-family: "Inter", sans-serif;
164 | }
165 |
166 | @supports (font-variation-settings: normal) {
167 | html {
168 | font-family: "Inter var", sans-serif;
169 | }
170 | }
171 |
172 | .app {
173 | @apply relative w-full h-screen overflow-hidden;
174 | }
175 |
176 | .home {
177 | @apply w-fit xl:h-full flex xl:justify-between justify-start items-start flex-col xl:py-8 xl:px-36 sm:p-8 p-6 max-xl:gap-7 absolute z-10;
178 | }
179 |
180 | .home-content {
181 | @apply flex-1 xl:justify-center justify-start flex flex-col gap-10;
182 | }
183 |
184 | .head-text {
185 | @apply xl:text-[10rem] text-[6rem] xl:leading-[11rem] leading-[7rem] font-black text-black;
186 | }
187 |
188 | .download-btn {
189 | @apply w-14 h-14 flex justify-center items-center rounded-full glassmorphism cursor-pointer outline-none;
190 | }
191 |
192 | .editortabs-container {
193 | @apply glassmorphism w-16 border-[2px] rounded-lg flex flex-col justify-center items-center ml-1 py-4 gap-4;
194 | }
195 |
196 | .filtertabs-container {
197 | @apply absolute z-10 bottom-5 right-0 left-0 w-full flex justify-center items-center flex-wrap gap-4;
198 | }
199 |
200 | .aipicker-container {
201 | @apply absolute left-full ml-3 glassmorphism p-3 w-[195px] h-[220px] rounded-md flex flex-col gap-4;
202 | }
203 |
204 | .aipicker-textarea {
205 | @apply w-full bg-transparent text-sm border border-gray-300 p-2 outline-none flex-1;
206 | }
207 |
208 | .filepicker-container {
209 | @apply absolute left-full ml-3 glassmorphism p-3 w-[195px] h-[220px] flex flex-col rounded-md;
210 | }
211 |
212 | .filepicker-label {
213 | @apply border border-gray-300 py-1.5 px-2 rounded-md shadow-sm text-xs text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 cursor-pointer w-fit;
214 | }
215 |
216 | .tab-btn {
217 | @apply w-14 h-14 flex justify-center items-center cursor-pointer select-none;
218 | }
219 |
220 | .glassmorphism {
221 | background: rgba(255, 255, 255, 0.25);
222 | box-shadow: 0 2px 30px 0 rgba(31, 38, 135, 0.07);
223 | backdrop-filter: blur(4px);
224 | -webkit-backdrop-filter: blur(4px);
225 | border: 1px solid rgba(255, 255, 255, 0.18);
226 | }
227 |
228 | input[type="file"] {
229 | z-index: -1;
230 | position: absolute;
231 | opacity: 0;
232 | }
233 |
234 | .sketch-picker {
235 | width: 170px !important;
236 | background: rgba(255, 255, 255, 0.25) !important;
237 | box-shadow: 0 2px 30px 0 rgba(31, 38, 135, 0.07) !important;
238 | backdrop-filter: blur(4px) !important;
239 | -webkit-backdrop-filter: blur(4px) !important;
240 | border: 1px solid rgba(255, 255, 255, 0.18) !important;
241 | border-radius: 6px !important;
242 | }
243 |
244 | .sketch-picker > div:nth-child(3) {
245 | display: none !important;
246 | }
247 | ```
248 |
249 |
250 | ## 🔗 Links
251 |
252 | Assets used in the project are [here](https://drive.google.com/drive/folders/166wA5NsMV_5D8NN7ujDDbPXC1X65vf2I)
253 |
254 | ## 🚀 More
255 |
256 | **Advance your skills with Next.js 14 Pro Course**
257 |
258 | Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go!
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 | **Accelerate your professional journey with the Expert Training program**
268 |
269 | And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together!
270 |
271 |
272 |
273 |
274 |
275 | #
276 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@react-three/drei": "^9.58.5",
13 | "@react-three/fiber": "^8.12.0",
14 | "framer-motion": "^10.9.4",
15 | "maath": "^0.5.3",
16 | "react": "^18.2.0",
17 | "react-color": "^2.19.3",
18 | "react-dom": "^18.2.0",
19 | "three": "^0.150.1",
20 | "valtio": "^1.10.3"
21 | },
22 | "devDependencies": {
23 | "@types/react": "^18.0.28",
24 | "@types/react-dom": "^18.0.11",
25 | "@vitejs/plugin-react": "^3.1.0",
26 | "autoprefixer": "^10.4.14",
27 | "postcss": "^8.4.21",
28 | "tailwindcss": "^3.3.0",
29 | "vite": "^4.2.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/client/public/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/public/react.png
--------------------------------------------------------------------------------
/client/public/shirt_baked.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/public/shirt_baked.glb
--------------------------------------------------------------------------------
/client/public/threejs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/public/threejs.png
--------------------------------------------------------------------------------
/client/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Canvas from './canvas';
2 | import Customizer from './pages/Customizer';
3 | import Home from './pages/Home';
4 |
5 | function App() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | export default App
16 |
--------------------------------------------------------------------------------
/client/src/assets/ai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/src/assets/ai.png
--------------------------------------------------------------------------------
/client/src/assets/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/src/assets/download.png
--------------------------------------------------------------------------------
/client/src/assets/file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/src/assets/file.png
--------------------------------------------------------------------------------
/client/src/assets/index.js:
--------------------------------------------------------------------------------
1 | import ai from "./ai.png";
2 | import fileIcon from "./file.png";
3 | import swatch from "./swatch.png";
4 | import download from "./download.png";
5 |
6 | import logoShirt from "./logo-tshirt.png";
7 | import stylishShirt from "./stylish-tshirt.png";
8 |
9 | export { ai, fileIcon, swatch, download, logoShirt, stylishShirt };
10 |
--------------------------------------------------------------------------------
/client/src/assets/logo-tshirt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/src/assets/logo-tshirt.png
--------------------------------------------------------------------------------
/client/src/assets/stylish-tshirt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/src/assets/stylish-tshirt.png
--------------------------------------------------------------------------------
/client/src/assets/swatch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/project_threejs_ai/bbe1d55b16267a9d115555be8ed2e2a2e2bb957b/client/src/assets/swatch.png
--------------------------------------------------------------------------------
/client/src/canvas/Backdrop.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 | import { easing } from 'maath'
3 | import { useFrame } from '@react-three/fiber'
4 | import { AccumulativeShadows, RandomizedLight } from '@react-three/drei';
5 |
6 | const Backdrop = () => {
7 | const shadows = useRef();
8 |
9 | return (
10 |
19 |
26 |
33 |
34 | )
35 | }
36 |
37 | export default Backdrop
--------------------------------------------------------------------------------
/client/src/canvas/CameraRig.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { useFrame } from '@react-three/fiber';
3 | import { easing } from 'maath';
4 | import { useSnapshot } from 'valtio';
5 |
6 | import state from '../store';
7 |
8 | const CameraRig = ({ children }) => {
9 | const group = useRef();
10 | const snap = useSnapshot(state);
11 |
12 | useFrame((state, delta) => {
13 | const isBreakpoint = window.innerWidth <= 1260;
14 | const isMobile = window.innerWidth <= 600;
15 |
16 | // set the initial position of the model
17 | let targetPosition = [-0.4, 0, 2];
18 | if(snap.intro) {
19 | if(isBreakpoint) targetPosition = [0, 0, 2];
20 | if(isMobile) targetPosition = [0, 0.2, 2.5];
21 | } else {
22 | if(isMobile) targetPosition = [0, 0, 2.5]
23 | else targetPosition = [0, 0, 2];
24 | }
25 |
26 | // set model camera position
27 | easing.damp3(state.camera.position, targetPosition, 0.25, delta)
28 |
29 | // set the model rotation smoothly
30 | easing.dampE(
31 | group.current.rotation,
32 | [state.pointer.y / 10, -state.pointer.x / 5, 0],
33 | 0.25,
34 | delta
35 | )
36 | })
37 |
38 |
39 | return {children}
40 | }
41 |
42 | export default CameraRig
--------------------------------------------------------------------------------
/client/src/canvas/Shirt.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { easing } from 'maath';
3 | import { useSnapshot } from 'valtio';
4 | import { useFrame } from '@react-three/fiber';
5 | import { Decal, useGLTF, useTexture } from '@react-three/drei';
6 |
7 | import state from '../store';
8 |
9 | const Shirt = () => {
10 | const snap = useSnapshot(state);
11 | const { nodes, materials } = useGLTF('/shirt_baked.glb');
12 |
13 | const logoTexture = useTexture(snap.logoDecal);
14 | const fullTexture = useTexture(snap.fullDecal);
15 |
16 | useFrame((state, delta) => easing.dampC(materials.lambert1.color, snap.color, 0.25, delta));
17 |
18 | const stateString = JSON.stringify(snap);
19 |
20 | return (
21 |
22 |
29 | {snap.isFullTexture && (
30 |
36 | )}
37 |
38 | {snap.isLogoTexture && (
39 |
48 | )}
49 |
50 |
51 | )
52 | }
53 |
54 | export default Shirt
--------------------------------------------------------------------------------
/client/src/canvas/index.jsx:
--------------------------------------------------------------------------------
1 | import { Canvas } from '@react-three/fiber'
2 | import { Environment, Center } from '@react-three/drei';
3 |
4 | import Shirt from './Shirt';
5 | import Backdrop from './Backdrop';
6 | import CameraRig from './CameraRig';
7 |
8 | const CanvasModel = () => {
9 | return (
10 |
26 | )
27 | }
28 |
29 | export default CanvasModel
--------------------------------------------------------------------------------
/client/src/components/AIPicker.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import CustomButton from './CustomButton';
4 |
5 | const AIPicker = ({ prompt, setPrompt, generatingImg, handleSubmit }) => {
6 | return (
7 |
41 | )
42 | }
43 |
44 | export default AIPicker
--------------------------------------------------------------------------------
/client/src/components/ColorPicker.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { SketchPicker } from 'react-color'
3 | import { useSnapshot } from 'valtio'
4 |
5 | import state from '../store';
6 |
7 | const ColorPicker = () => {
8 | const snap = useSnapshot(state);
9 |
10 | return (
11 |
12 | state.color = color.hex}
16 | />
17 |
18 | )
19 | }
20 |
21 | export default ColorPicker
--------------------------------------------------------------------------------
/client/src/components/CustomButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSnapshot } from 'valtio';
3 |
4 | import state from '../store';
5 | import { getContrastingColor } from '../config/helpers';
6 |
7 | const CustomButton = ({ type, title, customStyles, handleClick }) => {
8 | const snap = useSnapshot(state);
9 |
10 | const generateStyle = (type) => {
11 | if(type === 'filled') {
12 | return {
13 | backgroundColor: snap.color,
14 | color: getContrastingColor(snap.color)
15 | }
16 | } else if(type === "outline") {
17 | return {
18 | borderWidth: '1px',
19 | borderColor: snap.color,
20 | color: snap.color
21 | }
22 | }
23 | }
24 |
25 | return (
26 |
33 | )
34 | }
35 |
36 | export default CustomButton
--------------------------------------------------------------------------------
/client/src/components/FilePicker.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import CustomButton from './CustomButton'
4 |
5 | const FilePicker = ({ file, setFile, readFile }) => {
6 | return (
7 |
8 |
9 |
setFile(e.target.files[0])}
14 | />
15 |
18 |
19 |
20 | {file === '' ? "No file selected" : file.name}
21 |
22 |
23 |
24 |
25 | readFile('logo')}
29 | customStyles="text-xs"
30 | />
31 | readFile('full')}
35 | customStyles="text-xs"
36 | />
37 |
38 |
39 | )
40 | }
41 |
42 | export default FilePicker
--------------------------------------------------------------------------------
/client/src/components/Tab.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSnapshot } from 'valtio'
3 |
4 | import state from '../store';
5 |
6 | const Tab = ({ tab, isFilterTab, isActiveTab, handleClick }) => {
7 | const snap = useSnapshot(state);
8 |
9 | const activeStyles = isFilterTab && isActiveTab
10 | ? { backgroundColor: snap.color, opacity: 0.5 }
11 | : { backgroundColor: "transparent", opacity: 1 }
12 |
13 | return (
14 |
20 |

25 |
26 | )
27 | }
28 |
29 | export default Tab
--------------------------------------------------------------------------------
/client/src/components/index.js:
--------------------------------------------------------------------------------
1 | import CustomButton from "./CustomButton";
2 | import AIPicker from "./AIPicker";
3 | import ColorPicker from "./ColorPicker";
4 | import FilePicker from "./FilePicker";
5 | import Tab from "./Tab";
6 |
7 | export {
8 | CustomButton,
9 | AIPicker,
10 | ColorPicker,
11 | FilePicker,
12 | Tab,
13 | };
--------------------------------------------------------------------------------
/client/src/config/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | development: {
3 | backendUrl: "http://localhost:8080/api/v1/dalle",
4 | },
5 | production: {
6 | backendUrl: "https://devswag.onrender.com/api/v1/dalle",
7 | },
8 | };
9 |
10 | export default config;
11 |
--------------------------------------------------------------------------------
/client/src/config/constants.js:
--------------------------------------------------------------------------------
1 | import { swatch, fileIcon, ai, logoShirt, stylishShirt } from "../assets";
2 |
3 | export const EditorTabs = [
4 | {
5 | name: "colorpicker",
6 | icon: swatch,
7 | },
8 | {
9 | name: "filepicker",
10 | icon: fileIcon,
11 | },
12 | {
13 | name: "aipicker",
14 | icon: ai,
15 | },
16 | ];
17 |
18 | export const FilterTabs = [
19 | {
20 | name: "logoShirt",
21 | icon: logoShirt,
22 | },
23 | {
24 | name: "stylishShirt",
25 | icon: stylishShirt,
26 | },
27 | ];
28 |
29 | export const DecalTypes = {
30 | logo: {
31 | stateProperty: "logoDecal",
32 | filterTab: "logoShirt",
33 | },
34 | full: {
35 | stateProperty: "fullDecal",
36 | filterTab: "stylishShirt",
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/client/src/config/helpers.js:
--------------------------------------------------------------------------------
1 | export const downloadCanvasToImage = () => {
2 | const canvas = document.querySelector("canvas");
3 | const dataURL = canvas.toDataURL();
4 | const link = document.createElement("a");
5 |
6 | link.href = dataURL;
7 | link.download = "canvas.png";
8 | document.body.appendChild(link);
9 | link.click();
10 | document.body.removeChild(link);
11 | };
12 |
13 | export const reader = (file) =>
14 | new Promise((resolve, reject) => {
15 | const fileReader = new FileReader();
16 | fileReader.onload = () => resolve(fileReader.result);
17 | fileReader.readAsDataURL(file);
18 | });
19 |
20 | export const getContrastingColor = (color) => {
21 | // Remove the '#' character if it exists
22 | const hex = color.replace("#", "");
23 |
24 | // Convert the hex string to RGB values
25 | const r = parseInt(hex.substring(0, 2), 16);
26 | const g = parseInt(hex.substring(2, 4), 16);
27 | const b = parseInt(hex.substring(4, 6), 16);
28 |
29 | // Calculate the brightness of the color
30 | const brightness = (r * 299 + g * 587 + b * 114) / 1000;
31 |
32 | // Return black or white depending on the brightness
33 | return brightness > 128 ? "black" : "white";
34 | };
35 |
--------------------------------------------------------------------------------
/client/src/config/motion.js:
--------------------------------------------------------------------------------
1 | export const transition = { type: "spring", duration: 0.8 };
2 |
3 | export const slideAnimation = (direction) => {
4 | return {
5 | initial: {
6 | x: direction === "left" ? -100 : direction === "right" ? 100 : 0,
7 | y: direction === "up" ? 100 : direction === "down" ? -100 : 0,
8 | opacity: 0,
9 | transition: { ...transition, delay: 0.5 },
10 | },
11 | animate: {
12 | x: 0,
13 | y: 0,
14 | opacity: 1,
15 | transition: { ...transition, delay: 0 },
16 | },
17 | exit: {
18 | x: direction === "left" ? -100 : direction === "right" ? 100 : 0,
19 | y: direction === "up" ? 100 : direction === "down" ? -100 : 0,
20 | transition: { ...transition, delay: 0 },
21 | },
22 | };
23 | };
24 |
25 | export const fadeAnimation = {
26 | initial: {
27 | opacity: 0,
28 | transition: { ...transition, delay: 0.5 },
29 | },
30 | animate: {
31 | opacity: 1,
32 | transition: { ...transition, delay: 0 },
33 | },
34 | exit: {
35 | opacity: 0,
36 | transition: { ...transition, delay: 0 },
37 | },
38 | };
39 |
40 | export const headTextAnimation = {
41 | initial: { x: 100, opacity: 0 },
42 | animate: { x: 0, opacity: 1 },
43 | transition: {
44 | type: "spring",
45 | damping: 5,
46 | stiffness: 40,
47 | restDelta: 0.001,
48 | duration: 0.3,
49 | },
50 | };
51 |
52 | export const headContentAnimation = {
53 | initial: { y: 100, opacity: 0 },
54 | animate: { y: 0, opacity: 1 },
55 | transition: {
56 | type: "spring",
57 | damping: 7,
58 | stiffness: 30,
59 | restDelta: 0.001,
60 | duration: 0.6,
61 | delay: 0.2,
62 | delayChildren: 0.2,
63 | },
64 | };
65 |
66 | export const headContainerAnimation = {
67 | initial: { x: -100, opacity: 0, transition: { ...transition, delay: 0.5 } },
68 | animate: { x: 0, opacity: 1, transition: { ...transition, delay: 0 } },
69 | exit: { x: -100, opacity: 0, transition: { ...transition, delay: 0 } },
70 | };
71 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,200;0,600;1,900&display=swap");
2 | @import url("https://rsms.me/inter/inter.css");
3 |
4 | @tailwind base;
5 | @tailwind components;
6 | @tailwind utilities;
7 |
8 | html {
9 | font-family: "Inter", sans-serif;
10 | }
11 |
12 | @supports (font-variation-settings: normal) {
13 | html {
14 | font-family: "Inter var", sans-serif;
15 | }
16 | }
17 |
18 | .app {
19 | @apply relative w-full h-screen overflow-hidden;
20 | }
21 |
22 | .home {
23 | @apply w-fit xl:h-full flex xl:justify-between justify-start items-start flex-col xl:py-8 xl:px-36 sm:p-8 p-6 max-xl:gap-7 absolute z-10;
24 | }
25 |
26 | .home-content {
27 | @apply flex-1 xl:justify-center justify-start flex flex-col gap-10;
28 | }
29 |
30 | .head-text {
31 | @apply xl:text-[10rem] text-[6rem] xl:leading-[11rem] leading-[7rem] font-black text-black;
32 | }
33 |
34 | .download-btn {
35 | @apply w-14 h-14 flex justify-center items-center rounded-full glassmorphism cursor-pointer outline-none;
36 | }
37 |
38 | .editortabs-container {
39 | @apply glassmorphism w-16 border-[2px] rounded-lg flex flex-col justify-center items-center ml-1 py-4 gap-4;
40 | }
41 |
42 | .filtertabs-container {
43 | @apply absolute z-10 bottom-5 right-0 left-0 w-full flex justify-center items-center flex-wrap gap-4;
44 | }
45 |
46 | .aipicker-container {
47 | @apply absolute left-full ml-3 glassmorphism p-3 w-[195px] h-[220px] rounded-md flex flex-col gap-4;
48 | }
49 |
50 | .aipicker-textarea {
51 | @apply w-full bg-transparent text-sm border border-gray-300 p-2 outline-none flex-1;
52 | }
53 |
54 | .filepicker-container {
55 | @apply absolute left-full ml-3 glassmorphism p-3 w-[195px] h-[220px] flex flex-col rounded-md;
56 | }
57 |
58 | .filepicker-label {
59 | @apply border border-gray-300 py-1.5 px-2 rounded-md shadow-sm text-xs text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 cursor-pointer w-fit;
60 | }
61 |
62 | .tab-btn {
63 | @apply w-14 h-14 flex justify-center items-center cursor-pointer select-none;
64 | }
65 |
66 | .glassmorphism {
67 | background: rgba(255, 255, 255, 0.25);
68 | box-shadow: 0 2px 30px 0 rgba(31, 38, 135, 0.07);
69 | backdrop-filter: blur(4px);
70 | -webkit-backdrop-filter: blur(4px);
71 | border: 1px solid rgba(255, 255, 255, 0.18);
72 | }
73 |
74 | input[type="file"] {
75 | z-index: -1;
76 | position: absolute;
77 | opacity: 0;
78 | }
79 |
80 | .sketch-picker {
81 | width: 170px !important;
82 | background: rgba(255, 255, 255, 0.25) !important;
83 | box-shadow: 0 2px 30px 0 rgba(31, 38, 135, 0.07) !important;
84 | backdrop-filter: blur(4px) !important;
85 | -webkit-backdrop-filter: blur(4px) !important;
86 | border: 1px solid rgba(255, 255, 255, 0.18) !important;
87 | border-radius: 6px !important;
88 | }
89 |
90 | .sketch-picker > div:nth-child(3) {
91 | display: none !important;
92 | }
93 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/client/src/pages/Customizer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { AnimatePresence, motion } from 'framer-motion';
3 | import { useSnapshot } from 'valtio';
4 |
5 | import config from '../config/config';
6 | import state from '../store';
7 | import { download } from '../assets';
8 | import { downloadCanvasToImage, reader } from '../config/helpers';
9 | import { EditorTabs, FilterTabs, DecalTypes } from '../config/constants';
10 | import { fadeAnimation, slideAnimation } from '../config/motion';
11 | import { AIPicker, ColorPicker, CustomButton, FilePicker, Tab } from '../components';
12 |
13 | const Customizer = () => {
14 | const snap = useSnapshot(state);
15 |
16 | const [file, setFile] = useState('');
17 |
18 | const [prompt, setPrompt] = useState('');
19 | const [generatingImg, setGeneratingImg] = useState(false);
20 |
21 | const [activeEditorTab, setActiveEditorTab] = useState("");
22 | const [activeFilterTab, setActiveFilterTab] = useState({
23 | logoShirt: true,
24 | stylishShirt: false,
25 | })
26 |
27 | // show tab content depending on the activeTab
28 | const generateTabContent = () => {
29 | switch (activeEditorTab) {
30 | case "colorpicker":
31 | return
32 | case "filepicker":
33 | return
38 | case "aipicker":
39 | return
45 | default:
46 | return null;
47 | }
48 | }
49 |
50 | const handleSubmit = async (type) => {
51 | if(!prompt) return alert("Please enter a prompt");
52 |
53 | try {
54 | setGeneratingImg(true);
55 |
56 | const response = await fetch('http://localhost:8080/api/v1/dalle', {
57 | method: 'POST',
58 | headers: {
59 | 'Content-Type': 'application/json'
60 | },
61 | body: JSON.stringify({
62 | prompt,
63 | })
64 | })
65 |
66 | const data = await response.json();
67 |
68 | handleDecals(type, `data:image/png;base64,${data.photo}`)
69 | } catch (error) {
70 | alert(error)
71 | } finally {
72 | setGeneratingImg(false);
73 | setActiveEditorTab("");
74 | }
75 | }
76 |
77 | const handleDecals = (type, result) => {
78 | const decalType = DecalTypes[type];
79 |
80 | state[decalType.stateProperty] = result;
81 |
82 | if(!activeFilterTab[decalType.filterTab]) {
83 | handleActiveFilterTab(decalType.filterTab)
84 | }
85 | }
86 |
87 | const handleActiveFilterTab = (tabName) => {
88 | switch (tabName) {
89 | case "logoShirt":
90 | state.isLogoTexture = !activeFilterTab[tabName];
91 | break;
92 | case "stylishShirt":
93 | state.isFullTexture = !activeFilterTab[tabName];
94 | break;
95 | default:
96 | state.isLogoTexture = true;
97 | state.isFullTexture = false;
98 | break;
99 | }
100 |
101 | // after setting the state, activeFilterTab is updated
102 |
103 | setActiveFilterTab((prevState) => {
104 | return {
105 | ...prevState,
106 | [tabName]: !prevState[tabName]
107 | }
108 | })
109 | }
110 |
111 | const readFile = (type) => {
112 | reader(file)
113 | .then((result) => {
114 | handleDecals(type, result);
115 | setActiveEditorTab("");
116 | })
117 | }
118 |
119 | return (
120 |
121 | {!snap.intro && (
122 | <>
123 |
128 |
129 |
130 | {EditorTabs.map((tab) => (
131 | setActiveEditorTab(tab.name)}
135 | />
136 | ))}
137 |
138 | {generateTabContent()}
139 |
140 |
141 |
142 |
143 |
147 | state.intro = true}
151 | customStyles="w-fit px-4 py-2.5 font-bold text-sm"
152 | />
153 |
154 |
155 |
159 | {FilterTabs.map((tab) => (
160 | handleActiveFilterTab(tab.name)}
166 | />
167 | ))}
168 |
169 | >
170 | )}
171 |
172 | )
173 | }
174 |
175 | export default Customizer
--------------------------------------------------------------------------------
/client/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import { motion, AnimatePresence } from 'framer-motion';
2 | import { useSnapshot } from 'valtio';
3 |
4 | import state from '../store';
5 | import { CustomButton } from '../components';
6 | import {
7 | headContainerAnimation,
8 | headContentAnimation,
9 | headTextAnimation,
10 | slideAnimation
11 | } from '../config/motion';
12 |
13 | const Home = () => {
14 | const snap = useSnapshot(state);
15 |
16 | return (
17 |
18 | {snap.intro && (
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 | LET'S
DO IT.
32 |
33 |
34 |
38 |
39 | Create your unique and exclusive shirt with our brand-new 3D customization tool. Unleash your imagination{" "} and define your own style.
40 |
41 |
42 | state.intro = false}
46 | customStyles="w-fit px-4 py-2.5 font-bold text-sm"
47 | />
48 |
49 |
50 |
51 | )}
52 |
53 | )
54 | }
55 |
56 | export default Home
--------------------------------------------------------------------------------
/client/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { proxy } from 'valtio';
2 |
3 | const state = proxy({
4 | intro: true,
5 | color: '#EFBD48',
6 | isLogoTexture: true,
7 | isFullTexture: false,
8 | logoDecal: './threejs.png',
9 | fullDecal: './threejs.png',
10 | });
11 |
12 | export default state;
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import * as dotenv from 'dotenv';
3 | import cors from 'cors';
4 |
5 | import dalleRoutes from './routes/dalle.routes.js';
6 |
7 | dotenv.config();
8 |
9 | const app = express();
10 | app.use(cors());
11 | app.use(express.json({ limig: "50mb" }))
12 |
13 | app.use("/api/v1/dalle", dalleRoutes);
14 |
15 | app.get('/', (req, res) => {
16 | res.status(200).json({ message: "Hello from DALL.E" })
17 | })
18 |
19 | app.listen(8080, () => console.log('Server has started on port 8080'))
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "start": "nodemon index.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "cloudinary": "^1.35.0",
15 | "cors": "^2.8.5",
16 | "dotenv": "^16.0.3",
17 | "express": "^4.18.2",
18 | "mongoose": "^7.0.3",
19 | "nodemon": "^2.0.22",
20 | "openai": "^3.2.1"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/server/routes/dalle.routes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import * as dotenv from 'dotenv';
3 | import { Configuration, OpenAIApi} from 'openai';
4 |
5 | dotenv.config();
6 |
7 | const router = express.Router();
8 |
9 | const config = new Configuration({
10 | apiKey: process.env.OPENAI_API_KEY,
11 | });
12 |
13 | const openai = new OpenAIApi(config);
14 |
15 | router.route('/').get((req, res) => {
16 | res.status(200).json({ message: "Hello from DALL.E ROUTES" })
17 | })
18 |
19 | router.route('/').post(async (req, res) => {
20 | try {
21 | const { prompt } = req.body;
22 |
23 | const response = await openai.createImage({
24 | prompt,
25 | n: 1,
26 | size: '1024x1024',
27 | response_format: 'b64_json'
28 | });
29 |
30 | const image = response.data.data[0].b64_json;
31 |
32 | res.status(200).json({ photo: image });
33 | } catch (error) {
34 | console.error(error);
35 | res.status(500).json({ message: "Something went wrong" })
36 | }
37 | })
38 |
39 | export default router;
--------------------------------------------------------------------------------