Initial commit: React + Django full-stack project setup
- Backend: Django 4.2 + DRF + JWT + GraphQL - Frontend: React 18 + MobX + styled-components - Deployment: Docker + Docker Compose + Nginx - Database: PostgreSQL support - Documentation: README, INIT, PROJECT_DOCS, TESTING
This commit is contained in:
35
frontend/src/App.js
Normal file
35
frontend/src/App.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
const Header = styled.header`
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #eee;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
`;
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Container>
|
||||
<Header>
|
||||
<Title>React + Django App</Title>
|
||||
</Header>
|
||||
<Routes>
|
||||
<Route path="/" element={<div>Welcome to the app!</div>} />
|
||||
</Routes>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
27
frontend/src/index.js
Normal file
27
frontend/src/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'mobx-react-lite';
|
||||
import App from './App';
|
||||
import './styles/global';
|
||||
|
||||
// Import stores
|
||||
import UserStore from './stores/UserStore';
|
||||
import AuthStore from './stores/AuthStore';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
|
||||
const stores = {
|
||||
userStore: new UserStore(),
|
||||
authStore: new AuthStore(),
|
||||
};
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider {...stores}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
54
frontend/src/services/api.js
Normal file
54
frontend/src/services/api.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:8000',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add token to requests
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Handle token refresh
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refresh');
|
||||
const response = await axios.post('/api/token/refresh/', {
|
||||
refresh: refreshToken,
|
||||
});
|
||||
|
||||
const newToken = response.data.access;
|
||||
localStorage.setItem('token', newToken);
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
|
||||
return api(originalRequest);
|
||||
} catch (refreshError) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refresh');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
5
frontend/src/setupTests.js
Normal file
5
frontend/src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
44
frontend/src/stores/AuthStore.js
Normal file
44
frontend/src/stores/AuthStore.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import axios from 'axios';
|
||||
|
||||
class AuthStore {
|
||||
token = localStorage.getItem('token') || null;
|
||||
isAuthenticated = !!localStorage.getItem('token');
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
async login(email, password) {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login/', {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
this.token = response.data.access;
|
||||
this.isAuthenticated = true;
|
||||
localStorage.setItem('token', this.token);
|
||||
localStorage.setItem('refresh', response.data.refresh);
|
||||
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`;
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.detail || 'Login failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
this.isAuthenticated = false;
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refresh');
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthStore;
|
||||
32
frontend/src/stores/UserStore.js
Normal file
32
frontend/src/stores/UserStore.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import axios from 'axios';
|
||||
|
||||
class UserStore {
|
||||
user = null;
|
||||
loading = false;
|
||||
error = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
async fetchCurrentUser() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/users/me/');
|
||||
this.user = response.data;
|
||||
} catch (error) {
|
||||
this.error = error.response?.data || 'Failed to fetch user';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
clearUser() {
|
||||
this.user = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserStore;
|
||||
24
frontend/src/styles/global.js
Normal file
24
frontend/src/styles/global.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlobalStyle;
|
||||
Reference in New Issue
Block a user