केस का उपयोग करें
/
तीव्र विकास
MVP के विकास में तेजी लाएं
मॉडल
•
क्लाउड 3.7 सॉनेट
एक MVP SaaS नोट्स ऐप बनाएं—सुरक्षित लॉगिन, CRUD नोट्स, और Django + React का उपयोग करके टैग सर्च करें।
Django + React के साथ MVP SaaS नोट्स ऐप बनाना
बैकएंड के लिए Django और फ्रंटएंड के लिए React का उपयोग करके सुरक्षित लॉगिन, CRUD संचालन और टैग खोज कार्यक्षमता वाले नोट्स एप्लिकेशन के लिए न्यूनतम व्यवहार्य उत्पाद (MVP) बनाने की योजना यहां दी गई है।
आर्किटेक्चर अवलोकन
1Notes App
2├── Backend (Django)
3│ ├── Authentication
4│ ├── Notes API
5│ └── Search & Tags
6└── Frontend (React)
7 ├── Auth Components
8 ├── Note Management
9 └── Search Interfaceबैकएंड (Django) कार्यान्वयन
1। प्रोजेक्ट सेटअप
1# Install Django and required packages
2pip install django djangorestframework django-cors-headers djoser
3
4# Create Django project
5django-admin startproject notes_backend
6cd notes_backend
7
8# Create apps
9python manage.py startapp users
10python manage.py startapp notes2। सेटिंग कॉन्फ़िगर करें (settings.py)
1INSTALLED_APPS = [
2 # Django apps
3 'django.contrib.admin',
4 'django.contrib.auth',
5 'django.contrib.contenttypes',
6 'django.contrib.sessions',
7 'django.contrib.messages',
8 'django.contrib.staticfiles',
9
10 # Third-party apps
11 'rest_framework',
12 'corsheaders',
13 'djoser',
14
15 # Local apps
16 'users',
17 'notes',
18]
19
20MIDDLEWARE = [
21 'corsheaders.middleware.CorsMiddleware',
22 # Other middleware...
23]
24
25# CORS settings
26CORS_ALLOWED_ORIGINS = [
27 "http://localhost:3000", # React frontend
28]
29
30# REST Framework settings
31REST_FRAMEWORK = {
32 'DEFAULT_AUTHENTICATION_CLASSES': (
33 'rest_framework_simplejwt.authentication.JWTAuthentication',
34 ),
35 'DEFAULT_PERMISSION_CLASSES': [
36 'rest_framework.permissions.IsAuthenticated',
37 ]
38}
39
40# JWT settings
41SIMPLE_JWT = {
42 'AUTH_HEADER_TYPES': ('JWT',),
43 'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
44 'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
45}3। मॉडल (notes/models.py)
1from django.db import models
2from django.contrib.auth import get_user_model
3
4User = get_user_model()
5
6class Tag(models.Model):
7 name = models.CharField(max_length=50)
8 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tags')
9
10 def __str__(self):
11 return self.name
12
13 class Meta:
14 unique_together = ['name', 'user']
15
16class Note(models.Model):
17 title = models.CharField(max_length=200)
18 content = models.TextField()
19 created_at = models.DateTimeField(auto_now_add=True)
20 updated_at = models.DateTimeField(auto_now=True)
21 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notes')
22 tags = models.ManyToManyField(Tag, related_name='notes', blank=True)
23
24 def __str__(self):
25 return self.title
26
27 class Meta:
28 ordering = ['-updated_at']4। सीरियलाइज़र (notes/serializers.py)
1from rest_framework import serializers
2from .models import Note, Tag
3
4class TagSerializer(serializers.ModelSerializer):
5 class Meta:
6 model = Tag
7 fields = ['id', 'name']
8
9class NoteSerializer(serializers.ModelSerializer):
10 tags = TagSerializer(many=True, read_only=True)
11 tag_ids = serializers.PrimaryKeyRelatedField(
12 many=True,
13 write_only=True,
14 queryset=Tag.objects.all(),
15 source='tags',
16 required=False
17 )
18
19 class Meta:
20 model = Note
21 fields = ['id', 'title', 'content', 'created_at', 'updated_at', 'tags', 'tag_ids']
22 read_only_fields = ['created_at', 'updated_at']5। दृश्य (notes/views.py)
1from rest_framework import viewsets, filters
2from rest_framework.permissions import IsAuthenticated
3from django_filters.rest_framework import DjangoFilterBackend
4from .models import Note, Tag
5from .serializers import NoteSerializer, TagSerializer
6
7class TagViewSet(viewsets.ModelViewSet):
8 serializer_class = TagSerializer
9 permission_classes = [IsAuthenticated]
10
11 def get_queryset(self):
12 return Tag.objects.filter(user=self.request.user)
13
14 def perform_create(self, serializer):
15 serializer.save(user=self.request.user)
16
17class NoteViewSet(viewsets.ModelViewSet):
18 serializer_class = NoteSerializer
19 permission_classes = [IsAuthenticated]
20 filter_backends = [DjangoFilterBackend, filters.SearchFilter]
21 filterset_fields = ['tags__name']
22 search_fields = ['title', 'content', 'tags__name']
23
24 def get_queryset(self):
25 return Note.objects.filter(user=self.request.user)
26
27 def perform_create(self, serializer):
28 serializer.save(user=self.request.user)6। यूआरएल (notes_backend/urls.py)
1from django.contrib import admin
2from django.urls import path, include
3from rest_framework.routers import DefaultRouter
4from notes.views import NoteViewSet, TagViewSet
5
6router = DefaultRouter()
7router.register(r'notes', NoteViewSet, basename='note')
8router.register(r'tags', TagViewSet, basename='tag')
9
10urlpatterns = [
11 path('admin/', admin.site.urls),
12 path('api/', include(router.urls)),
13 path('api/auth/', include('djoser.urls')),
14 path('api/auth/', include('djoser.urls.jwt')),
15]7। माइग्रेशन चलाएँ
1python manage.py makemigrations
2python manage.py migrate
3python manage.py runserverफ्रंटएंड (रिएक्ट) कार्यान्वयन
1। प्रोजेक्ट सेटअप
1npx create-react-app notes-frontend
2cd notes-frontend
3npm install axios react-router-dom formik yup @mui/material @emotion/react @emotion/styled2। API सेवा (src/services/api.js)
1import axios from 'axios';
2
3const API_URL = 'http://localhost:8000/api/';
4
5// Create axios instance
6const api = axios.create({
7 baseURL: API_URL,
8 headers: {
9 'Content-Type': 'application/json',
10 },
11});
12
13// Add token to requests
14api.interceptors.request.use(
15 (config) => {
16 const token = localStorage.getItem('token');
17 if (token) {
18 config.headers['Authorization'] = `JWT ${token}`;
19 }
20 return config;
21 },
22 (error) => Promise.reject(error)
23);
24
25// Auth services
26export const login = (email, password) => {
27 return api.post('auth/jwt/create/', { email, password });
28};
29
30export const register = (email, password, name) => {
31 return api.post('auth/users/', { email, password, name });
32};
33
34// Note services
35export const getNotes = (search = '') => {
36 return api.get(`notes/${search ? `?search=${search}` : ''}`);
37};
38
39export const getNote = (id) => {
40 return api.get(`notes/${id}/`);
41};
42
43export const createNote = (note) => {
44 return api.post('notes/', note);
45};
46
47export const updateNote = (id, note) => {
48 return api.put(`notes/${id}/`, note);
49};
50
51export const deleteNote = (id) => {
52 return api.delete(`notes/${id}/`);
53};
54
55// Tag services
56export const getTags = () => {
57 return api.get('tags/');
58};
59
60export const createTag = (tag) => {
61 return api.post('tags/', tag);
62};
63
64export default api;3। प्रमाणीकरण प्रसंग (src/contexts/AuthContext.js)
1import React, { createContext, useState, useEffect, useContext } from 'react';
2import { login as apiLogin, register as apiRegister } from '../services/api';
3
4const AuthContext = createContext();
5
6export const AuthProvider = ({ children }) => {
7 const [user, setUser] = useState(null);
8 const [loading, setLoading] = useState(true);
9
10 useEffect(() => {
11 // Check if user is logged in
12 const token = localStorage.getItem('token');
13 if (token) {
14 setUser({ token });
15 }
16 setLoading(false);
17 }, []);
18
19 const login = async (email, password) => {
20 try {
21 const response = await apiLogin(email, password);
22 const { access } = response.data;
23 localStorage.setItem('token', access);
24 setUser({ token: access });
25 return true;
26 } catch (error) {
27 console.error('Login error:', error);
28 return false;
29 }
30 };
31
32 const register = async (email, password, name) => {
33 try {
34 await apiRegister(email, password, name);
35 return await login(email, password);
36 } catch (error) {
37 console.error('Registration error:', error);
38 return false;
39 }
40 };
41
42 const logout = () => {
43 localStorage.removeItem('token');
44 setUser(null);
45 };
46
47 return (
48 <AuthContext.Provider value={{ user, login, register, logout, loading }}>
49 {children}
50 </AuthContext.Provider>
51 );
52};
53
54export const useAuth = () => useContext(AuthContext);4। अवयव और संरचना
लॉगिन घटक (src/components/Login.js)
1import React from 'react';
2import { useFormik } from 'formik';
3import * as Yup from 'yup';
4import { TextField, Button, Typography, Box, Container } from '@mui/material';
5import { useAuth } from '../contexts/AuthContext';
6import { useNavigate } from 'react-router-dom';
7
8const Login = () => {
9 const { login } = useAuth();
10 const navigate = useNavigate();
11
12 const formik = useFormik({
13 initialValues: {
14 email: '',
15 password: '',
16 },
17 validationSchema: Yup.object({
18 email: Yup.string().email('Invalid email address').required('Required'),
19 password: Yup.string().required('Required'),
20 }),
21 onSubmit: async (values) => {
22 const success = await login(values.email, values.password);
23 if (success) {
24 navigate('/notes');
25 }
26 },
27 });
28
29 return (
30 <Container maxWidth="sm">
31 <Box sx={{ mt: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
32 <Typography component="h1" variant="h5">
33 Sign in
34 </Typography>
35 <Box component="form" onSubmit={formik.handleSubmit} sx={{ mt: 1 }}>
36 <TextField
37 margin="normal"
38 fullWidth
39 id="email"
40 label="Email Address"
41 name="email"
42 autoComplete="email"
43 value={formik.values.email}
44 onChange={formik.handleChange}
45 error={formik.touched.email && Boolean(formik.errors.email)}
46 helperText={formik.touched.email && formik.errors.email}
47 />
48 <TextField
49 margin="normal"
50 fullWidth
51 name="password"
52 label="Password"
53 type="password"
54 id="password"
55 autoComplete="current-password"
56 value={formik.values.password}
57 onChange={formik.handleChange}
58 error={formik.touched.password && Boolean(formik.errors.password)}
59 helperText={formik.touched.password && formik.errors.password}
60 />
61 <Button
62 type="submit"
63 fullWidth
64 variant="contained"
65 sx={{ mt: 3, mb: 2 }}
66 >
67 Sign In
68 </Button>
69 </Box>
70 </Box>
71 </Container>
72 );
73};
74
75export default Login;नोट्स सूची घटक (src/components/NotesList.js)
1import React, { useState, useEffect } from 'react';
2import {
3 Container, Typography, Box, List, ListItem, ListItemText,
4 IconButton, TextField, Button, Chip, Dialog, DialogTitle,
5 DialogContent, DialogActions
6} from '@mui/material';
7import { Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material';
8import { getNotes, deleteNote, getTags, createTag } from '../services/api';
9import { useNavigate } from 'react-router-dom';
10
11const NotesList = () => {
12 const [notes, setNotes] = useState([]);
13 const [tags, setTags] = useState([]);
14 const [search, setSearch] = useState('');
15 const [selectedTags, setSelectedTags] = useState([]);
16 const [openTagDialog, setOpenTagDialog] = useState(false);
17 const [newTagName, setNewTagName] = useState('');
18 const navigate = useNavigate();
19
20 useEffect(() => {
21 fetchNotes();
22 fetchTags();
23 }, [search, selectedTags]);
24
25 const fetchNotes = async () => {
26 try {
27 let searchQuery = search;
28 if (selectedTags.length > 0) {
29 const tagQuery = selectedTags.map(tag => `tags__name=${tag}`).join('&');
30 searchQuery = searchQuery ? `${searchQuery}&${tagQuery}` : tagQuery;
31 }
32 const response = await getNotes(searchQuery);
33 setNotes(response.data);
34 } catch (error) {
35 console.error('Error fetching notes:', error);
36 }
37 };
38
39 const fetchTags = async () => {
40 try {
41 const response = await getTags();
42 setTags(response.data);
43 } catch (error) {
44 console.error('Error fetching tags:', error);
45 }
46 };
47
48 const handleDeleteNote = async (id) => {
49 try {
50 await deleteNote(id);
51 setNotes(notes.filter(note => note.id !== id));
52 } catch (error) {
53 console.error('Error deleting note:', error);
54 }
55 };
56
57 const handleCreateTag = async () => {
58 if (newTagName.trim()) {
59 try {
60 await createTag({ name: newTagName.trim() });
61 setNewTagName('');
62 setOpenTagDialog(false);
63 fetchTags();
64 } catch (error) {
65 console.error('Error creating tag:', error);
66 }
67 }
68 };
69
70 const handleTagSelect = (tagName) => {
71 if (selectedTags.includes(tagName)) {
72 setSelectedTags(selectedTags.filter(tag => tag !== tagName));
73 } else {
74 setSelectedTags([...selectedTags, tagName]);
75 }
76 };
77
78 return (
79 <Container>
80 <Box sx={{ mt: 4, mb: 4 }}>
81 <Typography variant="h4" component="h1" gutterBottom>
82 My Notes
83 </Typography>
84
85 <Box sx={{ display: 'flex', mb: 2 }}>
86 <TextField
87 fullWidth
88 label="Search notes"
89 variant="outlined"
90 value={search}
91 onChange={(e) => setSearch(e.target.value)}
92 sx={{ mr: 2 }}
93 />
94 <Button
95 variant="contained"
96 startIcon={<AddIcon />}
97 onClick={() => navigate('/notes/new')}
98 >
99 New Note
100 </Button>
101 </Box>
102
103 <Box sx={{ mb: 2, display: 'flex', flexWrap: 'wrap', gap: 1 }}>
104 {tags.map(tag => (
105 <Chip
106 key={tag.id}
107 label={tag.name}
108 onClick={() => handleTagSelect(tag.name)}
109 color={selectedTags.includes(tag.name) ? "primary" : "default"}
110 />
111 ))}
112 <Chip
113 icon={<AddIcon />}
114 label="Add Tag"
115 onClick={() => setOpenTagDialog(true)}
116 variant="outlined"
117 />
118 </Box>
119
120 <List>
121 {notes.length > 0 ? (
122 notes.map((note) => (
123 <ListItem
124 key={note.id}
125 secondaryAction={
126 <Box>
127 <IconButton edge="end" onClick={() => navigate(`/notes/${note.id}`)}>
128 <EditIcon />
129 </IconButton>
130 <IconButton edge="end" onClick={() => handleDeleteNote(note.id)}>
131 <DeleteIcon />
132 </IconButton>
133 </Box>
134 }
135 sx={{
136 border: '1px solid #e0e0e0',
137 borderRadius: 1,
138 mb: 1,
139 '&:hover': { bgcolor: 'rgba(0, 0, 0, 0.04)' }
140 }}
141 >
142 <ListItemText
143 primary={note.title}
144 secondary={
145 <>
146 <Typography component="span" variant="body2" color="text.primary">
147 {note.content.substring(0, 100)}
148 {note.content.length > 100 ? '...' : ''}
149 </Typography>
150 <Box sx={{ mt: 1, display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
151 {note.tags.map(tag => (
152 <Chip key={tag.id} label={tag.name} size="small" />
153 ))}
154 </Box>
155 </>
156 }
157 />
158 </ListItem>
159 ))
160 ) : (
161 <Typography variant="body1">No notes found. Create your first note!</Typography>
162 )}
163 </List>
164 </Box>
165
166 <Dialog open={openTagDialog} onClose={() => setOpenTagDialog(false)}>
167 <DialogTitle>Create New Tag</DialogTitle>
168 <DialogContent>
169 <TextField
170 autoFocus
171 margin="dense"
172 label="Tag Name"
173 fullWidth
174 variant="outlined"
175 value={newTagName}
176 onChange={(e) => setNewTagName(e.target.value)}
177 />
178 </DialogContent>
179 <DialogActions>
180 <Button onClick={() => setOpenTagDialog(false)}>Cancel</Button>
181 <Button onClick={handleCreateTag}>Create</Button>
182 </DialogActions>
183 </Dialog>
184 </Container>
185 );
186};
187
188export default NotesList;नोट संपादक घटक (src/components/NoteEditor.js)
1import React, { useState, useEffect } from 'react';
2import { useParams, useNavigate } from 'react-router-dom';
3import { useFormik } from 'formik';
4import * as Yup from 'yup';
5import {
6 Container, Typography, Box, TextField, Button,
7 Chip, Select, MenuItem, FormControl, InputLabel,
8 OutlinedInput, ListItemText, Checkbox
9} from '@mui/material';
10import { getNote, createNote, updateNote, getTags } from '../services/api';
11
12const NoteEditor = () => {
13 const { id } = useParams();
14 const navigate = useNavigate();
15 const [tags, setTags] = useState([]);
16 const isEditing = Boolean(id);
17
18 useEffect(() => {
19 const fetchData = async () => {
20 try {
21 // Fetch available tags
22 const tagsResponse = await getTags();
23 setTags(tagsResponse.data);
24
25 // If editing, fetch note data
26 if (isEditing) {
27 const noteResponse = await getNote(id);
28 const note = noteResponse.data;
29 formik.setValues({
30 title: note.title,
31 content: note.content,
32 selectedTags: note.tags.map(tag => tag.id)
33 });
34 }
35 } catch (error) {
36 console.error('Error fetching data:', error);
37 }
38 };
39
40 fetchData();
41 }, [id]);
42
43 const formik = useFormik({
44 initialValues: {
45 title: '',
46 content: '',
47 selectedTags: []
48 },
49 validationSchema: Yup.object({
50 title: Yup.string().required('Title is required'),
51 content: Yup.string().required('Content is required'),
52 }),
53 onSubmit: async (values) => {
54 try {
55 const noteData = {
56 title: values.title,
57 content: values.content,
58 tag_ids: values.selectedTags
59 };
60
61 if (isEditing) {
62 await updateNote(id, noteData);
63 } else {
64 await createNote(noteData);
65 }
66 navigate('/notes');
67 } catch (error) {
68 console.error('Error saving note:', error);
69 }
70 },
71 });
72
73 return (
74 <Container>
75 <Box sx={{ mt: 4, mb: 4 }}>
76 <Typography variant="h4" component="h1" gutterBottom>
77 {isEditing ? 'Edit Note' : 'Create New Note'}
78 </Typography>
79
80 <Box component="form" onSubmit={formik.handleSubmit}>
81 <TextField
82 fullWidth
83 id="title"
84 name="title"
85 label="Title"
86 value={formik.values.title}
87 onChange={formik.handleChange}
88 error={formik.touched.title && Boolean(formik.errors.title)}
89 helperText={formik.touched.title && formik.errors.title}
90 margin="normal"
91 />
92
93 <TextField
94 fullWidth
95 id="content"
96 name="content"
97 label="Content"
98 multiline
99 rows={8}
100 value={formik.values.content}
101 onChange={formik.handleChange}
102 error={formik.touched.content && Boolean(formik.errors.content)}
103 helperText={formik.touched.content && formik.errors.content}
104 margin="normal"
105 />
106
107 <FormControl fullWidth margin="normal">
108 <InputLabel id="tags-label">Tags</InputLabel>
109 <Select
110 labelId="tags-label"
111 id="selectedTags"
112 name="selectedTags"
113 multiple
114 value={formik.values.selectedTags}
115 onChange={formik.handleChange}
116 input={<OutlinedInput label="Tags" />}
117 renderValue={(selected) => (
118 <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
119 {selected.map((tagId) => {
120 const tag = tags.find(t => t.id === tagId);
121 return tag ? (
122 <Chip key={tagId} label={tag.name} />
123 ) : null;
124 })}
125 </Box>
126 )}
127 >
128 {tags.map((tag) => (
129 <MenuItem key={tag.id} value={tag.id}>
130 <Checkbox checked={formik.values.selectedTags.indexOf(tag.id) > -1} />
131 <ListItemText primary={tag.name} />
132 </MenuItem>
133 ))}
134 </Select>
135 </FormControl>
136
137 <Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
138 <Button
139 variant="contained"
140 color="primary"
141 type="submit"
142 >
143 Save
144 </Button>
145 <Button
146 variant="outlined"
147 onClick={() => navigate('/notes')}
148 >
149 Cancel
150 </Button>
151 </Box>
152 </Box>
153 </Box>
154 </Container>
155 );
156};
157
158export default NoteEditor;5। ऐप रूटिंग (src/App.js)
1import React from 'react';
2import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
3import { AuthProvider, useAuth } from './contexts/AuthContext';
4import Login from './components/Login';
5import Register from './components/Register';
6import NotesList from './components/NotesList';
7import NoteEditor from './components/NoteEditor';
8import Navbar from './components/Navbar';
9import { CircularProgress, Box } from '@mui/material';
10
11// Protected route component
12const ProtectedRoute = ({ children }) => {
13 const { user, loading } = useAuth();
14
15 if (loading) {
16 return (
17 <Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
18 <CircularProgress />
19 </Box>
20 );
21 }
22
23 if (!user) {
24 return <Navigate to="/login" />;
25 }
26
27 return children;
28};
29
30function App() {
31 return (
32 <AuthProvider>
33 <Router>
34 <Navbar />
35 <Routes>
36 <Route path="/login" element={<Login />} />
37 <Route path="/register" element={<Register />} />
38 <Route
39 path="/notes"
40 element={
41 <ProtectedRoute>
42 <NotesList />
43 </ProtectedRoute>
44 }
45 />
46 <Route
47 path="/notes/new"
48 element={
49 <ProtectedRoute>
50 <NoteEditor />
51 </ProtectedRoute>
52 }
53 />
54 <Route
55 path="/notes/:id"
56 element={
57 <ProtectedRoute>
58 <NoteEditor />
59 </ProtectedRoute>
60 }
61 />
62 <Route path="/" element={<Navigate to="/notes" />} />
63 </Routes>
64 </Router>
65 </AuthProvider>
66 );
67}
68
69export default App;परिनियोजन संबंधी विचार
- बैकएंड परिनियोजन:
- Heroku, DigitalOcean, या AWS जैसी सेवा का उपयोग करें
- संवेदनशील जानकारी के लिए पर्यावरण चर सेट अप करें
- प्रोडक्शन फ्रंटएंड URL के लिए CORS को कॉन्फ़िगर करें
- उत्पादन डेटाबेस सेट करें (PostgreSQL अनुशंसित)
- फ्रंटएंड डिप्लॉयमेंट:
- Netlify, Vercel, या इसी तरह की सेवाओं पर तैनात करें
- API URL के लिए पर्यावरण चर कॉन्फ़िगर करें
- उचित बिल्ड स्क्रिप्ट सेट अप करें
- सुरक्षा संबंधी विचार:
- दर को सीमित करना लागू करें
- HTTPS का उपयोग करें
- सुरक्षित कुकी विशेषताएँ सेट करें
- बेहतर सुरक्षा के लिए 2FA जोड़ने पर विचार करें
एमवीपी के बाद अगले चरण
- उन्नत सुविधाएं:
- रिच टेक्स्ट एडिटिंग
- नोट शेयरिंग
- फ़ोल्डर/नोटबुक संगठन
- अटैचमेंट समर्थन
- प्रदर्शन में सुधार:
- नोट्स सूची के लिए पृष्ठांकन
- कैशिंग रणनीतियाँ
- आशावादी UI अपडेट
- व्यवसाय की विशेषताएं:
- यूज़र सब्सक्रिप्शन टियर
- उपयोग का विश्लेषण
- टीम सहयोग की विशेषताएं
यह MVP आपके द्वारा अनुरोधित सभी मुख्य कार्यक्षमताओं के साथ एक ठोस आधार प्रदान करता है: सुरक्षित प्रमाणीकरण, नोट्स के लिए CRUD संचालन और टैग-आधारित खोज क्षमताएं।
संबंधित उपयोग के मामलों का अन्वेषण करें
निंजा के AI सहायक का अनुभव करें
आज ही मुफ्त में आजमाएं। $19/माह से शुरू होने वाली योजनाएँ।


