├── .env ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── apple-touch-icon.svg ├── bookmark.svg ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── favicon.svg └── og-image.png ├── src ├── App.css ├── App.jsx ├── bookmarks.config.js ├── components │ ├── AddBookmark.jsx │ ├── Alert.jsx │ ├── Avatar.jsx │ ├── Badge.jsx │ ├── BookmarkCard.jsx │ ├── BookmarkModal.jsx │ ├── Dashboard.jsx │ ├── DeleteAccount.jsx │ ├── Dropdown.jsx │ ├── EmptyState.jsx │ ├── FeaturedCard.jsx │ ├── HeaderButton.jsx │ ├── IconButton.jsx │ ├── Icons.jsx │ ├── InputField.jsx │ ├── Logomark.jsx │ ├── ModalButton.jsx │ ├── ModalHeader.jsx │ ├── NavButton.jsx │ ├── PageHeader.jsx │ ├── Sidebar.jsx │ ├── SignIn.jsx │ ├── SignUp.jsx │ └── Textarea.jsx ├── firebase.js ├── index.css ├── main.jsx ├── pages │ ├── Bookmarks.jsx │ ├── Explore.jsx │ ├── Favorites.jsx │ └── Tag.jsx └── utils │ ├── addToFavorites.jsx │ ├── deleteFromBookmarks.jsx │ ├── deleteFromFavorites.jsx │ └── fetchLinkPreview.jsx ├── tailwind.config.js ├── thumbnail.png └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | VITE_API_KEY= 2 | VITE_AUTH_DOMAIN= 3 | VITE_DATABASE_URL= 4 | VITE_PROJECT_ID= 5 | VITE_STORAGE_BUCKET= 6 | VITE_MESSAGING_SENDER_ID= 7 | VITE_APP_ID= 8 | VITE_MEASUREMENT_ID= -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true, node: true }, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:react/jsx-runtime", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 10 | settings: { react: { version: "18.2" } }, 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": "warn", 14 | "react/prop-types": ["off"], 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.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 | .env 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | dist -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mustafa Pekkirişci 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 | # Bookmarks 2 | 3 | Bookmarks, yer işaretlerinizi paylaşmanızı kolaylaştırırken aynı zamanda yeni içerikleri keşfetmenize olanak tanıyan bir projedir. 4 | 5 | Bookmarks 6 | 7 | ## Özellikler 8 | 9 | - Firebase Authentication ile kullanıcılar e-posta ve şifre kullanarak uygulamaya kaydolabilir ve giriş yapabilirler. 10 | 11 | - Firebase Realtime Database kullanarak, kullanıcılar yer işaretlerini tüm kullanıcıların görüntüleyebileceği şekilde paylaşabilir. 12 | 13 | - Kullanıcılar kendi paylaştıkları yer işaretlerini silebilirler. 14 | 15 | - Kullanıcılar, kendi paylaştıkları veya diğer kullanıcıların paylaştığı yer işaretlerini favorilere ekleyebilirler. 16 | 17 | - Kullanıcılar, belirli bir etiketle işaretlenmiş yer işaretlerini görüntüleyebilirler. 18 | 19 | ## Kullanılan Teknolojiler 20 | 21 | - **Vite:** Proje geliştirme sürecinde Vite kullanılmıştır. 22 | 23 | - **React:** Kullanıcı arayüzünü oluşturmak için React kütüphanesi kullanılmıştır. 24 | 25 | - **Firebase:** Kullanıcı kimlik doğrulama, veritabanı yönetimi ve depolama gibi işlevleri sağlamak için Firebase platformu kullanılmıştır. 26 | 27 | - **Framer Motion:** Kullanıcı arayüzü animasyonları için Framer Motion kütüphanesi kullanılmıştır. 28 | 29 | - **Tailwind CSS:** Kullanıcı arayüzünün tasarımını oluşturmak için Tailwind CSS kullanılmıştır. 30 | 31 | ## Firebase Yapılandırması 32 | 33 | 1. [Firebase Console](https://console.firebase.google.com/)'a gidin. 34 | 35 | 2. Var olan bir Firebase projesi seçin veya yeni bir proje oluşturun. 36 | 37 | 3. Firebase Authentication bölümüne gidin ve "E-posta / Şifre" kimlik doğrulama yöntemini etkinleştirin. Bu, kullanıcıların e-posta ve şifre kullanarak kaydolmalarını ve giriş yapmalarını sağlayacaktır. 38 | 39 | 4. Firebase Realtime Database bölümüne gidin ve veritabanı oluşturma işlemi için gerekli adımları takip edin. 40 | 41 | 5. Veritabanı oluşturulduktan sonra, aşağıdaki veritabanı modelini ve kurallarını kullanarak devam edebilirsiniz. Bu kurallar, kullanıcıların yalnızca kendi yer işaretlerini silebilmelerini sağlar. Böylece kullanıcılar, diğer kullanıcıların yer işaretlerine müdahale edemezler. 42 | 43 | ```json 44 | { 45 | "bookmarks": { 46 | "bookmark1": { 47 | "description": "React projeniz için mükemmel yer tutucu avatarlar.", 48 | "id": "bookmark1", 49 | "likes": { 50 | "user1": true 51 | }, 52 | "tag": "react", 53 | "timestamp": 1685174317995, 54 | "title": "Avvvatars React", 55 | "url": "https://avvvatars.com/", 56 | "userDisplayName": "Mustafa", 57 | "userId": "user1" 58 | } 59 | }, 60 | "users": { 61 | "user1": { 62 | "bookmarks": { 63 | "bookmark1": { 64 | "description": "React projeniz için mükemmel yer tutucu avatarlar.", 65 | "id": "bookmark1", 66 | "likes": { 67 | "user1": true 68 | }, 69 | "tag": "react", 70 | "timestamp": 1685174317995, 71 | "title": "Avvvatars React", 72 | "url": "https://avvvatars.com/", 73 | "userDisplayName": "Mustafa", 74 | "userId": "user1" 75 | } 76 | }, 77 | "favorites": { 78 | "bookmark1": { 79 | "description": "React projeniz için mükemmel yer tutucu avatarlar.", 80 | "id": "bookmark1", 81 | "likes": { 82 | "user1": true 83 | }, 84 | "tag": "react", 85 | "timestamp": 1685174317995, 86 | "title": "Avvvatars React", 87 | "url": "https://avvvatars.com/", 88 | "userDisplayName": "Mustafa", 89 | "userId": "user1" 90 | } 91 | } 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | ```json 98 | { 99 | "rules": { 100 | "bookmarks": { 101 | ".read": true, 102 | ".indexOn": ["tag"], 103 | "$bookmarkId": { 104 | ".write": "auth != null && (newData.child('userId').val() === auth.uid || data.child('userId').val() === auth.uid)", 105 | "likes": { 106 | "$userId": { 107 | ".write": "auth.uid == $userId" 108 | } 109 | } 110 | } 111 | }, 112 | "users": { 113 | "$userId": { 114 | ".read": "auth != null && auth.uid == $userId", 115 | ".write": "auth != null && auth.uid == $userId" 116 | } 117 | } 118 | } 119 | } 120 | ``` 121 | 122 | 6. Firebase artık hazır. Proje ayarlarından Firebase proje yapılandırma bilgilerinizi alabilirsiniz. Bunlar, .env dosyasında Firebase yapılandırma değerlerini doldurmak için kullanılacak olan API anahtarı, proje kimliği vb. bilgilerdir. 123 | 124 | ## Bilgisayarınızda Çalıştırın 125 | 126 | Projeyi klonlayın. 127 | 128 | ```bash 129 | git clone https://github.com/pekkiriscim/bookmarks.git 130 | ``` 131 | 132 | Proje dizinine gidin. 133 | 134 | ```bash 135 | cd bookmarks 136 | ``` 137 | 138 | Gerekli paketleri yükleyin. 139 | 140 | ```bash 141 | npm install 142 | ``` 143 | 144 | Firebase projenize ait yapılandırmaları .env dosyasına ekleyin ve sunucuyu çalıştırın. 145 | 146 | ```bash 147 | npm run dev 148 | ``` 149 | 150 | Projeyi derleyin. 151 | 152 | ```bash 153 | npm run build 154 | ``` 155 | 156 | Önizlemeyi başlatın. 157 | 158 | ```bash 159 | npm run preview 160 | ``` 161 | 162 | ## Teşekkürler 163 | 164 | - [@nusu:](https://github.com/nusu) Benzersiz ve eğlenceli avatarlar için. 165 | 166 | - [@emilkowalski:](https://github.com/emilkowalski) Harika animasyonlu bildirimler için. 167 | 168 | ## Lisans 169 | 170 | Bu proje MIT Lisansı altında lisanslanmıştır. 171 | 172 | ## Katkı 173 | 174 | Katkılarınız projeyi daha da renklendirebilir. Deneyimlerinizi ve fikirlerinizi paylaşarak projenin gelişimine katkıda bulunun. 175 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 19 | 23 | 24 | 28 | 29 | Bookmarks 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookmarks", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "prettier": "npx prettier --write ." 12 | }, 13 | "dependencies": { 14 | "avvvatars-react": "^0.4.2", 15 | "firebase": "^9.21.0", 16 | "framer-motion": "^10.12.16", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "sonner": "^0.3.5" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.0.28", 23 | "@types/react-dom": "^18.0.11", 24 | "@vitejs/plugin-react": "^4.0.0", 25 | "autoprefixer": "^10.4.14", 26 | "eslint": "^8.38.0", 27 | "eslint-plugin-react": "^7.32.2", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "eslint-plugin-react-refresh": "^0.3.4", 30 | "postcss": "^8.4.23", 31 | "prettier": "2.8.8", 32 | "prettier-plugin-tailwindcss": "^0.2.7", 33 | "tailwindcss": "^3.3.1", 34 | "vite": "^4.3.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/apple-touch-icon.svg: -------------------------------------------------------------------------------- 1 | 8 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /public/bookmark.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekkiriscim/bookmarks/95d9d31b125d62115ae6d1b8f31a47c8b8f551c5/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekkiriscim/bookmarks/95d9d31b125d62115ae6d1b8f31a47c8b8f551c5/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekkiriscim/bookmarks/95d9d31b125d62115ae6d1b8f31a47c8b8f551c5/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 8 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekkiriscim/bookmarks/95d9d31b125d62115ae6d1b8f31a47c8b8f551c5/public/og-image.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekkiriscim/bookmarks/95d9d31b125d62115ae6d1b8f31a47c8b8f551c5/src/App.css -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import Dashboard from "./components/Dashboard"; 2 | 3 | function App() { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/bookmarks.config.js: -------------------------------------------------------------------------------- 1 | export const database_request_limit = 20; 2 | 3 | export const allowed_tags = { 4 | html: { color: "#B93815", backgroundColor: "#FEF6EE" }, 5 | css: { color: "#C11574", backgroundColor: "#FDF2FA" }, 6 | javascript: { color: "#B54708", backgroundColor: "#FFFAEB" }, 7 | react: { color: "#175CD3", backgroundColor: "#EFF8FF" }, 8 | "next.js": { color: "#344054", backgroundColor: "#F9FAFB" }, 9 | typescript: { color: "#3538CD", backgroundColor: "#EEF4FF" }, 10 | "vue.js": { color: "#067647", backgroundColor: "#ECFDF3" }, 11 | angular: { color: "#B42318", backgroundColor: "#FEF3F2" }, 12 | design: { color: "#5925DC", backgroundColor: "#F4F3FF" }, 13 | product: { color: "#C11574", backgroundColor: "#FDF2FA" }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/AddBookmark.jsx: -------------------------------------------------------------------------------- 1 | import { AddIcon } from "./Icons"; 2 | 3 | function AddBookmark({ onClick }) { 4 | return ( 5 | 14 | ); 15 | } 16 | 17 | export default AddBookmark; 18 | -------------------------------------------------------------------------------- /src/components/Alert.jsx: -------------------------------------------------------------------------------- 1 | function Alert({ title, description }) { 2 | return ( 3 |
4 | {title} 5 | {description} 6 |
7 | ); 8 | } 9 | 10 | export default Alert; 11 | -------------------------------------------------------------------------------- /src/components/Avatar.jsx: -------------------------------------------------------------------------------- 1 | import Avvvatars from "avvvatars-react"; 2 | 3 | function Avatar({ size, value, radius }) { 4 | return ( 5 |
6 |
7 | 8 |
9 | {value} 10 |
11 | ); 12 | } 13 | 14 | export default Avatar; 15 | -------------------------------------------------------------------------------- /src/components/Badge.jsx: -------------------------------------------------------------------------------- 1 | function Badge({ text, color, backgroundColor }) { 2 | return ( 3 |
7 | {text} 8 |
9 | ); 10 | } 11 | 12 | export default Badge; 13 | -------------------------------------------------------------------------------- /src/components/BookmarkCard.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useContext } from "react"; 2 | 3 | import { allowed_tags } from "../bookmarks.config"; 4 | 5 | import Badge from "./Badge"; 6 | import Avatar from "./Avatar"; 7 | import IconButton from "./IconButton"; 8 | 9 | import { HeartIcon, DeleteIcon, HeartFilledIcon } from "./Icons"; 10 | 11 | import { AuthStateContext } from "./Dashboard"; 12 | 13 | import { fetchLinkPreview } from "../utils/fetchLinkPreview"; 14 | import { deleteFromBookmarks } from "../utils/deleteFromBookmarks"; 15 | import { addToFavorites } from "../utils/addToFavorites"; 16 | import { deleteFromFavorites } from "../utils/deleteFromFavorites"; 17 | 18 | const badges = allowed_tags; 19 | 20 | function BookmarkCard({ bookmark }) { 21 | const [linkPreview, setLinkPreview] = useState(null); 22 | const [isDeletingFromBookmarks, setIsDeletingFromBookmarks] = useState(false); 23 | const [isUpdatingFavorites, setIsUpdatingFavorites] = useState(false); 24 | 25 | const { authState } = useContext(AuthStateContext); 26 | 27 | useEffect(() => { 28 | fetchLinkPreview(bookmark, setLinkPreview); 29 | }, [bookmark]); 30 | 31 | return ( 32 |
33 |
34 | 35 | {bookmark.title} 40 | 41 |
42 |
43 |
44 | 49 |
50 | 51 | 52 | {bookmark.title} 53 | 54 | 55 | 56 | {bookmark.description} 57 | 58 |
59 | 60 | {authState.isLoggedIn && authState.activeUser && ( 61 |
62 | {bookmark.userId === authState.activeUser.uid && ( 63 | { 66 | await deleteFromBookmarks( 67 | e, 68 | setIsDeletingFromBookmarks, 69 | bookmark, 70 | authState 71 | ); 72 | }} 73 | isLoading={isDeletingFromBookmarks} 74 | /> 75 | )} 76 | {bookmark.likes && 77 | bookmark.likes[authState.activeUser.uid] === true ? ( 78 | { 81 | await deleteFromFavorites( 82 | e, 83 | setIsUpdatingFavorites, 84 | authState, 85 | bookmark 86 | ); 87 | }} 88 | isLoading={isUpdatingFavorites} 89 | /> 90 | ) : ( 91 | { 94 | await addToFavorites( 95 | e, 96 | setIsUpdatingFavorites, 97 | authState, 98 | bookmark 99 | ); 100 | }} 101 | isLoading={isUpdatingFavorites} 102 | /> 103 | )} 104 |
105 | )} 106 |
107 |
108 |
109 | ); 110 | } 111 | 112 | export default BookmarkCard; 113 | -------------------------------------------------------------------------------- /src/components/BookmarkModal.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from "react"; 2 | 3 | import { allowed_tags } from "../bookmarks.config"; 4 | 5 | import ModalHeader from "./ModalHeader"; 6 | import Textarea from "./Textarea"; 7 | import InputField from "./InputField"; 8 | import ModalButton from "./ModalButton"; 9 | import Dropdown from "./Dropdown"; 10 | import Alert from "./Alert"; 11 | 12 | import { motion } from "framer-motion"; 13 | 14 | import { toast } from "sonner"; 15 | 16 | import { ModalContext, AuthStateContext } from "./Dashboard"; 17 | 18 | import { database } from "../firebase"; 19 | import { ref, push, set } from "firebase/database"; 20 | 21 | const badges = Object.keys(allowed_tags); 22 | 23 | function BookmarkModal({ forwardRef }) { 24 | const { modal, setModal } = useContext(ModalContext); 25 | const { authState } = useContext(AuthStateContext); 26 | 27 | const [newBookmark, setNewBookmark] = useState({ 28 | title: "", 29 | description: "", 30 | url: "", 31 | tag: "", 32 | userDisplayName: authState.activeUser.displayName, 33 | userId: authState.activeUser.uid, 34 | id: "", 35 | timestamp: Date.now(), 36 | }); 37 | 38 | const handleNewBookmark = async (e) => { 39 | e.preventDefault(); 40 | 41 | setModal({ ...modal, isLoading: true }); 42 | 43 | const bookmarksRef = push(ref(database, "bookmarks")); 44 | 45 | const userBookmarksRef = ref( 46 | database, 47 | `users/${authState.activeUser.uid}/bookmarks/${bookmarksRef.key}` 48 | ); 49 | 50 | try { 51 | await set(bookmarksRef, { ...newBookmark, id: bookmarksRef.key }); 52 | await set(userBookmarksRef, { ...newBookmark, id: bookmarksRef.key }); 53 | 54 | setModal({ activeModal: null, isLoading: false }); 55 | 56 | toast( 57 | 61 | ); 62 | } catch (error) { 63 | setModal({ ...modal, isLoading: false }); 64 | 65 | console.log(error); 66 | 67 | toast( 68 | 72 | ); 73 | } 74 | }; 75 | 76 | return ( 77 | 85 |
{ 89 | e.preventDefault(); 90 | 91 | toast( 92 | 96 | ); 97 | } 98 | : handleNewBookmark 99 | } 100 | className="grid gap-y-8" 101 | > 102 |
103 | 107 |
108 | 27 | {hint && ( 28 | {hint} 29 | )} 30 |
31 | ); 32 | } 33 | 34 | export default Textarea; 35 | -------------------------------------------------------------------------------- /src/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getAuth } from "firebase/auth"; 3 | import { getDatabase } from "firebase/database"; 4 | 5 | const firebaseConfig = { 6 | apiKey: import.meta.env.VITE_API_KEY, 7 | authDomain: import.meta.env.VITE_AUTH_DOMAIN, 8 | databaseURL: import.meta.env.VITE_DATABASE_URL, 9 | projectId: import.meta.env.VITE_PROJECT_ID, 10 | storageBucket: import.meta.env.VITE_STORAGE_BUCKET, 11 | messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID, 12 | appId: import.meta.env.VITE_APP_ID, 13 | measurementId: import.meta.env.VITE_MEASUREMENT_ID, 14 | }; 15 | 16 | const app = initializeApp(firebaseConfig); 17 | 18 | export const auth = getAuth(app); 19 | export const database = getDatabase(app); 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer base { 8 | input { 9 | @apply h-11 rounded-lg border border-gray-300 px-3.5 py-2.5 text-tmd font-regular text-gray-900 outline-none placeholder:text-gray-500 focus:border-primary-300 focus:ring-4 focus:ring-primary-100; 10 | } 11 | 12 | textarea { 13 | @apply resize-none rounded-lg border border-gray-300 px-3.5 py-2.5 text-tmd font-regular text-gray-900 outline-none placeholder:text-gray-500 focus:border-primary-300 focus:ring-4 focus:ring-primary-100; 14 | } 15 | 16 | html, 17 | body { 18 | @apply h-full w-full antialiased; 19 | text-rendering: optimizeLegibility; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | } 23 | 24 | @layer components { 25 | #root { 26 | @apply h-full w-full; 27 | } 28 | 29 | .bookmark-card:hover .icon-button { 30 | @apply flex; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/pages/Bookmarks.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | 3 | import BookmarkCard from "../components/BookmarkCard"; 4 | import EmptyState from "../components/EmptyState"; 5 | 6 | import { LoadingIcon } from "../components/Icons"; 7 | 8 | import { AuthStateContext } from "../components/Dashboard"; 9 | 10 | import { database } from "../firebase"; 11 | import { ref, onValue, off } from "firebase/database"; 12 | 13 | function Bookmarks() { 14 | const [isBookmarksLoading, setIsBookmarksLoading] = useState(true); 15 | const [bookmarks, setBookmarks] = useState(); 16 | 17 | const { authState } = useContext(AuthStateContext); 18 | 19 | useEffect(() => { 20 | const userBookmarksRef = ref( 21 | database, 22 | `users/${authState.activeUser.uid}/bookmarks` 23 | ); 24 | 25 | onValue(userBookmarksRef, (snapshot) => { 26 | const data = snapshot.val(); 27 | 28 | if (data && data !== null) { 29 | const bookmarkArray = []; 30 | 31 | Object.values(data).map((bookmark) => { 32 | bookmark.id && bookmarkArray.push(bookmark); 33 | }); 34 | 35 | if (bookmarkArray.length === 0) { 36 | setBookmarks(false); 37 | 38 | setIsBookmarksLoading(false); 39 | } else { 40 | bookmarkArray.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); 41 | 42 | setBookmarks(bookmarkArray); 43 | 44 | setIsBookmarksLoading(false); 45 | } 46 | } else { 47 | setBookmarks(false); 48 | 49 | setIsBookmarksLoading(false); 50 | } 51 | }); 52 | 53 | return () => { 54 | off(userBookmarksRef); 55 | }; 56 | }, [authState.activeUser.uid]); 57 | 58 | return ( 59 |
60 | {isBookmarksLoading ? ( 61 |
62 | 63 |
64 | ) : bookmarks === false ? ( 65 | 71 | ) : ( 72 | bookmarks.map((bookmark) => { 73 | if (bookmark.id) { 74 | return ; 75 | } 76 | }) 77 | )} 78 |
79 | ); 80 | } 81 | 82 | export default Bookmarks; 83 | -------------------------------------------------------------------------------- /src/pages/Explore.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | import { database_request_limit } from "../bookmarks.config"; 4 | 5 | import BookmarkCard from "../components/BookmarkCard"; 6 | import EmptyState from "../components/EmptyState"; 7 | 8 | import { LoadingIcon } from "../components/Icons"; 9 | 10 | import { database } from "../firebase"; 11 | import { ref, onValue, off, query, limitToLast } from "firebase/database"; 12 | 13 | function Explore() { 14 | const [isExploreLoading, setIsExploreLoading] = useState(true); 15 | const [bookmarks, setBookmarks] = useState(); 16 | 17 | useEffect(() => { 18 | const bookmarksRef = query( 19 | ref(database, "bookmarks"), 20 | limitToLast(database_request_limit) 21 | ); 22 | 23 | onValue(bookmarksRef, (snapshot) => { 24 | const data = snapshot.val(); 25 | 26 | if (data && data !== null) { 27 | const bookmarkArray = []; 28 | 29 | Object.values(data).map((bookmark) => { 30 | bookmark.id && bookmarkArray.push(bookmark); 31 | }); 32 | 33 | if (bookmarkArray.length === 0) { 34 | setBookmarks(false); 35 | 36 | setIsExploreLoading(false); 37 | } else { 38 | bookmarkArray.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); 39 | 40 | setBookmarks(bookmarkArray); 41 | 42 | setIsExploreLoading(false); 43 | } 44 | } else { 45 | setBookmarks(false); 46 | 47 | setIsExploreLoading(false); 48 | } 49 | }); 50 | 51 | return () => { 52 | off(bookmarksRef); 53 | }; 54 | }, []); 55 | 56 | return ( 57 |
58 | {isExploreLoading ? ( 59 |
60 | 61 |
62 | ) : bookmarks === false ? ( 63 | 69 | ) : ( 70 | bookmarks.map((bookmark) => { 71 | if (bookmark.id) { 72 | return ; 73 | } 74 | }) 75 | )} 76 |
77 | ); 78 | } 79 | 80 | export default Explore; 81 | -------------------------------------------------------------------------------- /src/pages/Favorites.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | 3 | import BookmarkCard from "../components/BookmarkCard"; 4 | import EmptyState from "../components/EmptyState"; 5 | 6 | import { LoadingIcon } from "../components/Icons"; 7 | 8 | import { AuthStateContext } from "../components/Dashboard"; 9 | 10 | import { database } from "../firebase"; 11 | import { ref, onValue, off } from "firebase/database"; 12 | 13 | function Favorites() { 14 | const [isFavoritesLoading, setIsFavoritesLoading] = useState(true); 15 | const [bookmarks, setBookmarks] = useState(); 16 | 17 | const { authState } = useContext(AuthStateContext); 18 | 19 | useEffect(() => { 20 | const userFavoritesRef = ref( 21 | database, 22 | `users/${authState.activeUser.uid}/favorites` 23 | ); 24 | 25 | onValue(userFavoritesRef, (snapshot) => { 26 | const data = snapshot.val(); 27 | 28 | if (data && data !== null) { 29 | const bookmarkArray = Object.values(data); 30 | bookmarkArray.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); 31 | 32 | setBookmarks(bookmarkArray); 33 | 34 | setIsFavoritesLoading(false); 35 | } else { 36 | setBookmarks(false); 37 | 38 | setIsFavoritesLoading(false); 39 | } 40 | }); 41 | 42 | return () => { 43 | off(userFavoritesRef); 44 | }; 45 | }, [authState.activeUser.uid]); 46 | 47 | return ( 48 |
49 | {isFavoritesLoading ? ( 50 |
51 | 52 |
53 | ) : bookmarks === false ? ( 54 | 60 | ) : ( 61 | bookmarks.map((bookmark) => { 62 | if (bookmark.id) { 63 | return ; 64 | } 65 | }) 66 | )} 67 |
68 | ); 69 | } 70 | 71 | export default Favorites; 72 | -------------------------------------------------------------------------------- /src/pages/Tag.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | import BookmarkCard from "../components/BookmarkCard"; 4 | import EmptyState from "../components/EmptyState"; 5 | 6 | import { LoadingIcon } from "../components/Icons"; 7 | 8 | import { database } from "../firebase"; 9 | import { 10 | query, 11 | ref, 12 | orderByChild, 13 | equalTo, 14 | onValue, 15 | off, 16 | } from "firebase/database"; 17 | 18 | function Tag({ tag }) { 19 | const [isTagLoading, setIsTagLoading] = useState(true); 20 | const [bookmarks, setBookmarks] = useState(); 21 | 22 | useEffect(() => { 23 | const bookmarksRef = query( 24 | ref(database, "bookmarks"), 25 | orderByChild("tag"), 26 | equalTo(tag) 27 | ); 28 | 29 | onValue(bookmarksRef, (snapshot) => { 30 | const data = snapshot.val(); 31 | 32 | if (data && data !== null) { 33 | const bookmarkArray = Object.values(data); 34 | bookmarkArray.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); 35 | 36 | setBookmarks(bookmarkArray); 37 | 38 | setIsTagLoading(false); 39 | } else { 40 | setBookmarks(false); 41 | 42 | setIsTagLoading(false); 43 | } 44 | }); 45 | 46 | return () => { 47 | off(bookmarksRef); 48 | }; 49 | }, [tag]); 50 | 51 | return ( 52 |
53 | {isTagLoading ? ( 54 |
55 | 56 |
57 | ) : bookmarks === false ? ( 58 | 62 | ) : ( 63 | bookmarks.map((bookmark) => { 64 | if (bookmark.id) { 65 | return ; 66 | } 67 | }) 68 | )} 69 |
70 | ); 71 | } 72 | 73 | export default Tag; 74 | -------------------------------------------------------------------------------- /src/utils/addToFavorites.jsx: -------------------------------------------------------------------------------- 1 | import Alert from "../components/Alert"; 2 | 3 | import { toast } from "sonner"; 4 | 5 | import { database } from "../firebase"; 6 | import { ref, set } from "firebase/database"; 7 | 8 | export const addToFavorites = async ( 9 | e, 10 | setIsUpdatingFavorites, 11 | authState, 12 | bookmark 13 | ) => { 14 | e.preventDefault(); 15 | 16 | setIsUpdatingFavorites(true); 17 | 18 | const userFavoritesRef = ref( 19 | database, 20 | `users/${authState.activeUser.uid}/favorites/${bookmark.id}` 21 | ); 22 | const userBookmarksRef = ref( 23 | database, 24 | `users/${authState.activeUser.uid}/bookmarks/${bookmark.id}` 25 | ); 26 | const bookmarkLikesRef = ref( 27 | database, 28 | `bookmarks/${bookmark.id}/likes/${authState.activeUser.uid}` 29 | ); 30 | 31 | try { 32 | await set(userFavoritesRef, { 33 | ...bookmark, 34 | likes: { [authState.activeUser.uid]: true }, 35 | }); 36 | 37 | if (bookmark.userId === authState.activeUser.uid) { 38 | await set(userBookmarksRef, { 39 | ...bookmark, 40 | likes: { [authState.activeUser.uid]: true }, 41 | }); 42 | } 43 | 44 | await set(bookmarkLikesRef, true); 45 | 46 | setIsUpdatingFavorites(false); 47 | 48 | toast( 49 | 53 | ); 54 | } catch (error) { 55 | setIsUpdatingFavorites(false); 56 | 57 | console.log(error); 58 | 59 | toast( 60 | 64 | ); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/utils/deleteFromBookmarks.jsx: -------------------------------------------------------------------------------- 1 | import Alert from "../components/Alert"; 2 | 3 | import { toast } from "sonner"; 4 | 5 | import { database } from "../firebase"; 6 | import { ref, set } from "firebase/database"; 7 | 8 | export const deleteFromBookmarks = async ( 9 | e, 10 | setIsDeletingFromBookmarks, 11 | bookmark, 12 | authState 13 | ) => { 14 | e.preventDefault(); 15 | 16 | setIsDeletingFromBookmarks(true); 17 | 18 | const bookmarksRef = ref(database, `bookmarks/${bookmark.id}`); 19 | const userBookmarksRef = ref( 20 | database, 21 | `users/${authState.activeUser.uid}/bookmarks/${bookmark.id}` 22 | ); 23 | 24 | try { 25 | await set(bookmarksRef, null); 26 | await set(userBookmarksRef, null); 27 | 28 | setIsDeletingFromBookmarks(false); 29 | 30 | toast( 31 | 37 | ); 38 | } catch (error) { 39 | setIsDeletingFromBookmarks(false); 40 | 41 | console.log(error); 42 | 43 | toast( 44 | 48 | ); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils/deleteFromFavorites.jsx: -------------------------------------------------------------------------------- 1 | import Alert from "../components/Alert"; 2 | 3 | import { toast } from "sonner"; 4 | 5 | import { database } from "../firebase"; 6 | import { ref, set } from "firebase/database"; 7 | 8 | export const deleteFromFavorites = async ( 9 | e, 10 | setIsUpdatingFavorites, 11 | authState, 12 | bookmark 13 | ) => { 14 | e.preventDefault(); 15 | 16 | setIsUpdatingFavorites(true); 17 | 18 | const userFavoritesRef = ref( 19 | database, 20 | `users/${authState.activeUser.uid}/favorites/${bookmark.id}` 21 | ); 22 | const bookmarkLikesRef = ref( 23 | database, 24 | `bookmarks/${bookmark.id}/likes/${authState.activeUser.uid}` 25 | ); 26 | const userBookmarksRef = ref( 27 | database, 28 | `users/${authState.activeUser.uid}/bookmarks/${bookmark.id}/likes/${authState.activeUser.uid}` 29 | ); 30 | 31 | try { 32 | await set(userFavoritesRef, null); 33 | await set(bookmarkLikesRef, false); 34 | 35 | if (bookmark.userId === authState.activeUser.uid) { 36 | await set(userBookmarksRef, false); 37 | } 38 | 39 | setIsUpdatingFavorites(false); 40 | 41 | toast( 42 | 46 | ); 47 | } catch (error) { 48 | setIsUpdatingFavorites(false); 49 | 50 | console.log(error); 51 | 52 | toast( 53 | 57 | ); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/utils/fetchLinkPreview.jsx: -------------------------------------------------------------------------------- 1 | export const fetchLinkPreview = async (bookmark, setLinkPreview) => { 2 | const encodingReferance = { 3 | "%": "%25", 4 | "!": "%21", 5 | "*": "%2A", 6 | "'": "%27", 7 | "(": "%28", 8 | ")": "%29", 9 | ";": "%3B", 10 | ":": "%3A", 11 | "@": "%40", 12 | "&": "%26", 13 | "=": "%3D", 14 | "+": "%2B", 15 | ",": "%2C", 16 | "/": "%2F", 17 | "?": "%3F", 18 | "#": "%23", 19 | "[": "%5B", 20 | "]": "%5D", 21 | $: "%24", 22 | }; 23 | 24 | try { 25 | let encodedUrl = bookmark.url; 26 | 27 | Object.entries(encodingReferance).map(([character, replacement]) => { 28 | encodedUrl = encodedUrl.replaceAll(character, replacement); 29 | }); 30 | 31 | const linkPreview = "https://rdl.ink/render/" + encodedUrl; 32 | 33 | const response = await fetch(linkPreview, { 34 | origin: "http://localhost:5173/", 35 | }); 36 | 37 | if (response.status === 200) { 38 | const blob = await response.blob(); 39 | 40 | setLinkPreview(URL.createObjectURL(blob)); 41 | } 42 | } catch (error) { 43 | console.log(error); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | colors: { 6 | primary: { 7 | 25: "#F5FBFF", 8 | 50: "#F0F9FF", 9 | 100: "#E0F2FE", 10 | 200: "#B9E6FE", 11 | 300: "#7CD4FD", 12 | 400: "#36BFFA", 13 | 500: "#0BA5EC", 14 | 600: "#0086C9", 15 | 700: "#026AA2", 16 | 800: "#065986", 17 | 900: "#0B4A6F", 18 | }, 19 | 20 | gray: { 21 | 25: "#FCFCFD", 22 | 50: "#F9FAFB", 23 | 100: "#F2F4F7", 24 | 200: "#EAECF0", 25 | 300: "#D0D5DD", 26 | 400: "#98A2B3", 27 | 500: "#667085", 28 | 600: "#475467", 29 | 700: "#344054", 30 | 800: "#1D2939", 31 | 900: "#101828", 32 | }, 33 | 34 | danger: { 35 | 25: "#FFFBFA", 36 | 50: "#FEF3F2", 37 | 100: "#FEE4E2", 38 | 200: "#FECDCA", 39 | 300: "#FDA29B", 40 | 400: "#F97066", 41 | 500: "#F04438", 42 | 600: "#D92D20", 43 | 700: "#B42318", 44 | 800: "#912018", 45 | 900: "#7A271A", 46 | }, 47 | 48 | black: "#000000", 49 | 50 | white: "#FFFFFF", 51 | }, 52 | 53 | fontFamily: { 54 | sans: [ 55 | "Inter", 56 | "ui-sans-serif", 57 | "system-ui", 58 | "-apple-system", 59 | "BlinkMacSystemFont", 60 | "Segoe UI", 61 | "Roboto", 62 | "Helvetica Neue", 63 | "Arial", 64 | "Noto Sans", 65 | "sans-serif", 66 | "Apple Color Emoji", 67 | "Segoe UI Emoji", 68 | "Segoe UI Symbol", 69 | "Noto Color Emoji", 70 | ], 71 | }, 72 | 73 | fontSize: { 74 | txs: ["0.75rem", "1.125rem"], 75 | tsm: ["0.875rem", "1.25rem"], 76 | tmd: ["1rem", "1.5rem"], 77 | tlg: ["1.125rem", "1.75rem"], 78 | txl: ["1.25rem", "1.875rem"], 79 | dxs: ["1.5rem", "2rem"], 80 | dsm: ["1.875rem", "2.375rem"], 81 | dmd: ["2.25rem", "2.75rem"], 82 | dlg: ["3rem", "3.75rem"], 83 | dxl: ["3.75rem", "4.5rem"], 84 | d2xl: ["4.5rem", "5.625rem"], 85 | }, 86 | 87 | fontWeight: { 88 | regular: "400", 89 | medium: "500", 90 | semibold: "600", 91 | bold: "700", 92 | }, 93 | 94 | extend: {}, 95 | }, 96 | plugins: [], 97 | }; 98 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekkiriscim/bookmarks/95d9d31b125d62115ae6d1b8f31a47c8b8f551c5/thumbnail.png -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | export default defineConfig({ 5 | server: { host: true }, 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------