├── .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 |
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 |
17 |
--------------------------------------------------------------------------------
/public/bookmark.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
155 |
156 | );
157 | }
158 |
159 | export default BookmarkModal;
160 |
--------------------------------------------------------------------------------
/src/components/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { useState, createContext, useRef, useEffect } from "react";
2 |
3 | import Sidebar from "./Sidebar";
4 | import PageHeader from "./PageHeader";
5 | import Alert from "./Alert";
6 |
7 | import { LoadingIcon } from "./Icons";
8 |
9 | import { motion, AnimatePresence } from "framer-motion";
10 |
11 | import Explore from "../pages/Explore";
12 | import Bookmarks from "../pages/Bookmarks";
13 | import Favorites from "../pages/Favorites";
14 | import Tag from "../pages/Tag";
15 |
16 | import SignUp from "./SignUp";
17 | import SignIn from "./SignIn";
18 | import BookmarkModal from "./BookmarkModal";
19 | import DeleteAccount from "./DeleteAccount";
20 |
21 | import { Toaster, toast } from "sonner";
22 |
23 | import { auth } from "../firebase";
24 | import { onAuthStateChanged } from "firebase/auth";
25 |
26 | export const PageContext = createContext();
27 | export const ModalContext = createContext();
28 | export const AuthStateContext = createContext();
29 | export const MobileSidebarContext = createContext();
30 |
31 | function Dashboard() {
32 | const [isAppLoading, setIsAppLoading] = useState(true);
33 | const [page, setPage] = useState("explore");
34 | const [modal, setModal] = useState({ activeModal: null, isLoading: false });
35 | const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
36 | const [authState, setAuthState] = useState({
37 | isLoggedIn: false,
38 | activeUser: null,
39 | });
40 |
41 | useEffect(() => {
42 | onAuthStateChanged(auth, (user) => {
43 | if (user) {
44 | setAuthState({ isLoggedIn: true, activeUser: user });
45 |
46 | setIsAppLoading(false);
47 | } else {
48 | setAuthState({ isLoggedIn: false, activeUser: null });
49 |
50 | setIsAppLoading(false);
51 | }
52 | });
53 | }, [authState.isLoggedIn]);
54 |
55 | const modalContainerRef = useRef();
56 |
57 | const handleModal = (e) => {
58 | if (!modalContainerRef.current.contains(e.target))
59 | setModal({ ...modal, activeModal: null });
60 | };
61 |
62 | const modals = {
63 | signUp: ,
64 | signIn: ,
65 | bookmark: ,
66 | deleteAccount: ,
67 | };
68 |
69 | const pages = {
70 | explore: ,
71 | bookmarks: ,
72 | favorites: ,
73 | };
74 |
75 | return (
76 |
79 |
80 |
81 | {isAppLoading ? (
82 |
83 |
84 |
85 | ) : (
86 | <>
87 |
88 |
89 | {modals[modal.activeModal] && (
90 | {
98 | toast(
99 |
105 | );
106 | }
107 | : handleModal
108 | }
109 | className="absolute z-20 flex h-full w-full cursor-pointer items-center justify-center overflow-auto bg-gray-700 bg-opacity-70 backdrop-blur max-sm:flex-col max-sm:justify-end max-sm:p-4"
110 | >
111 | {modals[modal.activeModal]}
112 |
113 | )}
114 |
115 |
116 |
122 |
123 |
124 |
125 | {pages[page] ? pages[page] :
}
126 |
127 |
128 |
129 | >
130 | )}
131 |
132 |
133 |
134 | );
135 | }
136 |
137 | export default Dashboard;
138 |
--------------------------------------------------------------------------------
/src/components/DeleteAccount.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import Alert from "./Alert";
4 | import ModalHeader from "./ModalHeader";
5 | import ModalButton from "./ModalButton";
6 |
7 | import { motion } from "framer-motion";
8 |
9 | import { toast } from "sonner";
10 |
11 | import { ModalContext, AuthStateContext, PageContext } from "./Dashboard";
12 |
13 | import { auth, database } from "../firebase";
14 | import { get, ref, set } from "firebase/database";
15 | import { deleteUser } from "firebase/auth";
16 |
17 | function DeleteAccount({ forwardRef }) {
18 | const { modal, setModal } = useContext(ModalContext);
19 | const { authState, setAuthState } = useContext(AuthStateContext);
20 | const { setPage } = useContext(PageContext);
21 |
22 | const handleDeleteAccount = async (e) => {
23 | try {
24 | e.preventDefault();
25 |
26 | setModal({ ...modal, isLoading: true });
27 |
28 | const userBookmarksRef = ref(
29 | database,
30 | `users/${authState.activeUser.uid}/bookmarks`
31 | );
32 | const usersRef = ref(database, `users/${authState.activeUser.uid}`);
33 |
34 | const snapshot = await get(userBookmarksRef);
35 | if (snapshot.exists()) {
36 | const userBookmarks = Object.keys(snapshot.val());
37 |
38 | userBookmarks.map(async (bookmark) => {
39 | const bookmarksRef = ref(database, `bookmarks/${bookmark}`);
40 |
41 | await set(bookmarksRef, null);
42 | });
43 | } else {
44 | console.log("Veri yok.");
45 | }
46 |
47 | await set(usersRef, null);
48 | await deleteUser(auth.currentUser);
49 |
50 | setPage("explore");
51 | setModal({ activeModal: null, isLoading: false });
52 | setAuthState({ isLoggedIn: false, activeUser: null });
53 |
54 | toast(
55 |
61 | );
62 | } catch (error) {
63 | setModal({ ...modal, isLoading: false });
64 |
65 | if (error.code === "auth/requires-recent-login") {
66 | toast(
67 |
73 | );
74 | } else {
75 | toast(
76 |
80 | );
81 | }
82 | }
83 | };
84 |
85 | return (
86 |
94 |
107 |
108 | );
109 | }
110 |
111 | export default DeleteAccount;
112 |
--------------------------------------------------------------------------------
/src/components/Dropdown.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import { SearchIcon, TickIcon } from "./Icons";
4 |
5 | import { motion, AnimatePresence } from "framer-motion";
6 |
7 | function Dropdown({ text, label, hint, badges, newBookmark, setNewBookmark }) {
8 | const [isOpen, setIsOpen] = useState(false);
9 |
10 | return (
11 |
12 |
18 |
19 |
39 |
40 | {isOpen && (
41 |
48 | {badges.map((element, index) => {
49 | return (
50 |
67 | );
68 | })}
69 |
70 | )}
71 |
72 |
73 | {hint && (
74 |
{hint}
75 | )}
76 |
77 | );
78 | }
79 |
80 | export default Dropdown;
81 |
--------------------------------------------------------------------------------
/src/components/EmptyState.jsx:
--------------------------------------------------------------------------------
1 | function EmptyState({ title, description }) {
2 | return (
3 |
4 |
21 |
22 |
23 | {title}
24 |
25 |
26 | {description}
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default EmptyState;
34 |
--------------------------------------------------------------------------------
/src/components/FeaturedCard.jsx:
--------------------------------------------------------------------------------
1 | function FeaturedCard() {
2 | return (
3 |
4 |
5 |
6 | Projeye Katkıda Bulunun
7 |
8 |
9 | Bu proje açık kaynaklıdır. Birlikte geliştirmek için katkıda bulunun.
10 |
11 |
12 |
18 | Katkıda Bulun
19 |
20 |
21 | );
22 | }
23 |
24 | export default FeaturedCard;
25 |
--------------------------------------------------------------------------------
/src/components/HeaderButton.jsx:
--------------------------------------------------------------------------------
1 | function HeaderButton({ text, isPrimary, onClick }) {
2 | return (
3 |
13 | );
14 | }
15 |
16 | export default HeaderButton;
17 |
--------------------------------------------------------------------------------
/src/components/IconButton.jsx:
--------------------------------------------------------------------------------
1 | import { LoadingIcon } from "./Icons";
2 |
3 | function IconButton({ Icon, onClick, isLoading }) {
4 | return (
5 |
12 | );
13 | }
14 |
15 | export default IconButton;
16 |
--------------------------------------------------------------------------------
/src/components/Icons.jsx:
--------------------------------------------------------------------------------
1 | export function ExploreIcon() {
2 | return (
3 |
22 | );
23 | }
24 |
25 | export function BookmarksIcon() {
26 | return (
27 |
41 | );
42 | }
43 |
44 | export function FavoritesIcon() {
45 | return (
46 |
60 | );
61 | }
62 |
63 | export function SignOutIcon() {
64 | return (
65 |
79 | );
80 | }
81 |
82 | export function AddIcon() {
83 | return (
84 |
97 | );
98 | }
99 |
100 | export function HeartIcon() {
101 | return (
102 |
116 | );
117 | }
118 |
119 | export function HeartFilledIcon() {
120 | return (
121 |
132 | );
133 | }
134 |
135 | export function DeleteIcon() {
136 | return (
137 |
150 | );
151 | }
152 |
153 | export function SearchIcon() {
154 | return (
155 |
166 | );
167 | }
168 |
169 | export function TickIcon() {
170 | return (
171 |
185 | );
186 | }
187 |
188 | export function LoadingIcon({ light }) {
189 | return (
190 |
229 | );
230 | }
231 |
232 | export function MenuIcon() {
233 | return (
234 |
247 | );
248 | }
249 |
250 | export function CloseIcon() {
251 | return (
252 |
265 | );
266 | }
267 |
268 | export function DeleteAccountIcon() {
269 | return (
270 |
283 | );
284 | }
285 |
--------------------------------------------------------------------------------
/src/components/InputField.jsx:
--------------------------------------------------------------------------------
1 | function InputField({
2 | inputID,
3 | label,
4 | type,
5 | hint,
6 | placeholder,
7 | min,
8 | max,
9 | onChange,
10 | }) {
11 | return (
12 |
13 |
16 |
27 | {hint && (
28 | {hint}
29 | )}
30 |
31 | );
32 | }
33 |
34 | export default InputField;
35 |
--------------------------------------------------------------------------------
/src/components/Logomark.jsx:
--------------------------------------------------------------------------------
1 | import Avvvatars from "avvvatars-react";
2 |
3 | function Logomark({ size, value, radius }) {
4 | return (
5 |
6 | {value && value !== "" ? (
7 |
8 | ) : (
9 |
30 | )}
31 |
32 | );
33 | }
34 |
35 | export default Logomark;
36 |
--------------------------------------------------------------------------------
/src/components/ModalButton.jsx:
--------------------------------------------------------------------------------
1 | import { LoadingIcon } from "./Icons";
2 |
3 | function ModalButton({ text, isLoading, danger }) {
4 | return (
5 |
16 | );
17 | }
18 |
19 | export default ModalButton;
20 |
--------------------------------------------------------------------------------
/src/components/ModalHeader.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import Alert from "./Alert";
4 | import Logomark from "./Logomark";
5 |
6 | import { CloseIcon } from "./Icons";
7 |
8 | import { toast } from "sonner";
9 |
10 | import { ModalContext } from "./Dashboard";
11 |
12 | function ModalHeader({ title, description, avvvatars }) {
13 | const { modal, setModal } = useContext(ModalContext);
14 |
15 | return (
16 |
17 |
18 |
19 |
43 |
44 |
45 | {title}
46 |
47 | {description}
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default ModalHeader;
55 |
--------------------------------------------------------------------------------
/src/components/NavButton.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { PageContext, MobileSidebarContext } from "./Dashboard";
4 |
5 | function NavButton({ Icon, text, route, onClick }) {
6 | const { page, setPage } = useContext(PageContext);
7 | const { setIsMobileSidebarOpen } = useContext(MobileSidebarContext);
8 |
9 | const handleClick = () => {
10 | route && setPage(route);
11 |
12 | setIsMobileSidebarOpen(false);
13 | };
14 |
15 | return (
16 |
25 | );
26 | }
27 |
28 | export default NavButton;
29 |
--------------------------------------------------------------------------------
/src/components/PageHeader.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import HeaderButton from "./HeaderButton";
4 |
5 | import { MenuIcon } from "./Icons";
6 |
7 | import {
8 | PageContext,
9 | ModalContext,
10 | AuthStateContext,
11 | MobileSidebarContext,
12 | } from "./Dashboard";
13 |
14 | function PageHeader() {
15 | const { page } = useContext(PageContext);
16 | const { modal, setModal } = useContext(ModalContext);
17 | const { authState } = useContext(AuthStateContext);
18 | const { setIsMobileSidebarOpen } = useContext(MobileSidebarContext);
19 |
20 | const pageTranslations = {
21 | explore: "Keşfet",
22 | bookmarks: "Yer İşaretleri",
23 | favorites: "Favoriler",
24 | };
25 |
26 | return (
27 |
28 |
29 |
37 |
38 | {pageTranslations[page] ? pageTranslations[page] : `#${page}`}
39 |
40 |
41 | {!(authState.isLoggedIn && authState.activeUser) && (
42 |
43 | {
47 | setModal({ ...modal, activeModal: "signIn" });
48 | }}
49 | />
50 | {
54 | setModal({ ...modal, activeModal: "signUp" });
55 | }}
56 | />
57 |
58 | )}
59 |
60 | );
61 | }
62 |
63 | export default PageHeader;
64 |
--------------------------------------------------------------------------------
/src/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useRef } from "react";
2 |
3 | import { allowed_tags } from "../bookmarks.config";
4 |
5 | import Alert from "./Alert";
6 | import Avatar from "./Avatar";
7 | import NavButton from "./NavButton";
8 | import AddBookmark from "./AddBookmark";
9 | import Logomark from "./Logomark";
10 | import FeaturedCard from "./FeaturedCard";
11 | import {
12 | ExploreIcon,
13 | BookmarksIcon,
14 | FavoritesIcon,
15 | SignOutIcon,
16 | CloseIcon,
17 | DeleteAccountIcon,
18 | } from "./Icons";
19 |
20 | import { toast } from "sonner";
21 |
22 | import {
23 | ModalContext,
24 | AuthStateContext,
25 | MobileSidebarContext,
26 | PageContext,
27 | } from "./Dashboard";
28 |
29 | import { auth } from "../firebase";
30 | import { signOut } from "firebase/auth";
31 |
32 | const tags = Object.keys(allowed_tags);
33 |
34 | function Sidebar() {
35 | const { modal, setModal } = useContext(ModalContext);
36 | const { authState, setAuthState } = useContext(AuthStateContext);
37 | const { setPage } = useContext(PageContext);
38 | const { isMobileSidebarOpen, setIsMobileSidebarOpen } =
39 | useContext(MobileSidebarContext);
40 |
41 | const handleSignOut = async () => {
42 | await signOut(auth)
43 | .then(() => {
44 | setPage("explore");
45 |
46 | setAuthState({ ...authState, isLoggedIn: false });
47 |
48 | setIsMobileSidebarOpen(false);
49 |
50 | toast(
51 |
55 | );
56 | })
57 | .catch((error) => {
58 | console.log(error);
59 |
60 | toast(
61 |
65 | );
66 | });
67 | };
68 |
69 | const sidebarRef = useRef();
70 |
71 | const handleSidebar = (e) => {
72 | if (!sidebarRef.current.contains(e.target)) setIsMobileSidebarOpen(false);
73 | };
74 |
75 | return (
76 |
82 |
86 |
87 |
88 | {authState.isLoggedIn && authState.activeUser ? (
89 |
94 | ) : (
95 |
96 | )}
97 |
105 |
106 | {authState.isLoggedIn && authState.activeUser && (
107 |
108 |
{
110 | setModal({ ...modal, activeModal: "bookmark" });
111 | }}
112 | />
113 |
114 | )}
115 |
116 |
117 | {tags.map((tag, index) => {
118 | return ;
119 | })}
120 | {
128 | setModal({ ...modal, activeModal: "signUp" });
129 | }
130 | }
131 | />
132 | {
140 | setModal({ ...modal, activeModal: "signUp" });
141 | }
142 | }
143 | />
144 |
145 |
146 |
147 |
148 | {authState.isLoggedIn && authState.activeUser && (
149 |
150 | {
154 | setModal({ ...modal, activeModal: "deleteAccount" });
155 | }}
156 | />
157 |
162 |
163 | )}
164 |
165 |
166 |
167 | );
168 | }
169 |
170 | export default Sidebar;
171 |
--------------------------------------------------------------------------------
/src/components/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import Alert from "./Alert";
4 | import ModalHeader from "./ModalHeader";
5 | import InputField from "./InputField";
6 | import ModalButton from "./ModalButton";
7 |
8 | import { motion } from "framer-motion";
9 |
10 | import { toast } from "sonner";
11 |
12 | import { ModalContext } from "./Dashboard";
13 |
14 | import { auth } from "../firebase";
15 | import { signInWithEmailAndPassword } from "firebase/auth";
16 |
17 | function SignIn({ forwardRef }) {
18 | const [signIn, setSignIn] = useState({
19 | email: "",
20 | password: "",
21 | });
22 |
23 | const { modal, setModal } = useContext(ModalContext);
24 |
25 | const handleSignIn = async (e) => {
26 | e.preventDefault();
27 |
28 | setModal({ ...modal, isLoading: true });
29 |
30 | await signInWithEmailAndPassword(auth, signIn.email, signIn.password)
31 | .then(() => {
32 | setModal({ activeModal: null, isLoading: false });
33 |
34 | toast(
35 |
39 | );
40 | })
41 | .catch((error) => {
42 | setModal({ ...modal, isLoading: false });
43 |
44 | console.log(error);
45 |
46 | toast(
47 |
51 | );
52 | });
53 | };
54 |
55 | const handleEmail = (e) => {
56 | setSignIn({ ...signIn, email: e.target.value });
57 | };
58 |
59 | const handlePassword = (e) => {
60 | setSignIn({ ...signIn, password: e.target.value });
61 | };
62 |
63 | return (
64 |
72 |
103 |
104 | );
105 | }
106 |
107 | export default SignIn;
108 |
--------------------------------------------------------------------------------
/src/components/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from "react";
2 |
3 | import Alert from "./Alert";
4 | import InputField from "./InputField";
5 | import ModalHeader from "./ModalHeader";
6 | import ModalButton from "./ModalButton";
7 |
8 | import { motion } from "framer-motion";
9 |
10 | import { toast } from "sonner";
11 |
12 | import { ModalContext } from "./Dashboard";
13 |
14 | import { auth } from "../firebase";
15 | import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth";
16 |
17 | function SignUp({ forwardRef }) {
18 | const [signUp, setSignUp] = useState({
19 | fullName: "",
20 | email: "",
21 | password: "",
22 | });
23 |
24 | const { modal, setModal } = useContext(ModalContext);
25 |
26 | const updateDisplayName = async () => {
27 | await updateProfile(auth.currentUser, {
28 | displayName: signUp.fullName,
29 | });
30 | };
31 |
32 | const handleSignUp = async (e) => {
33 | e.preventDefault();
34 |
35 | setModal({ ...modal, isLoading: true });
36 |
37 | await createUserWithEmailAndPassword(auth, signUp.email, signUp.password)
38 | .then(async () => {
39 | await updateDisplayName();
40 |
41 | setModal({ activeModal: null, isLoading: false });
42 |
43 | toast(
44 |
50 | );
51 | })
52 | .catch((error) => {
53 | setModal({ ...modal, isLoading: false });
54 |
55 | console.log(error);
56 |
57 | toast(
58 |
62 | );
63 | });
64 | };
65 |
66 | const handleFullName = (e) => {
67 | setSignUp({ ...signUp, fullName: e.target.value });
68 | };
69 |
70 | const handleEmail = (e) => {
71 | setSignUp({ ...signUp, email: e.target.value });
72 | };
73 |
74 | const handlePassword = (e) => {
75 | setSignUp({ ...signUp, password: e.target.value });
76 | };
77 |
78 | return (
79 |
87 |
131 |
132 | );
133 | }
134 |
135 | export default SignUp;
136 |
--------------------------------------------------------------------------------
/src/components/Textarea.jsx:
--------------------------------------------------------------------------------
1 | function Textarea({
2 | inputID,
3 | label,
4 | hint,
5 | placeholder,
6 | min,
7 | max,
8 | onChange,
9 | rows,
10 | }) {
11 | return (
12 |
13 |
16 |
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 |
--------------------------------------------------------------------------------